diff --git a/.gitignore b/.gitignore index ec93b8c0f4..7d2347577a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ build/ .deps/ .libs/ +.cache/ libtool config.log diff --git a/CMakeLists.txt b/CMakeLists.txt index de8661512b..7a2c4663de 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,8 @@ project( VERSION "${T8CODE_VERSION_MAJOR}.${T8CODE_VERSION_MINOR}.${T8CODE_VERSION_PATCH}" ) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + include( GNUInstallDirs) include( CTest ) include( CMakeDependentOption ) @@ -53,6 +55,7 @@ option( T8CODE_BUILD_FORTRAN_INTERFACE "Build t8code's Fortran interface" OFF ) option( T8CODE_ENABLE_MPI "Enable t8code's features which rely on MPI" ON ) option( T8CODE_ENABLE_VTK "Enable t8code's features which rely on VTK" OFF ) +option( T8CODE_ENABLE_MRA "Enable t8code's features which rely on MRA" OFF ) option( T8CODE_ENABLE_OCC "Enable t8code's features which rely on OpenCASCADE" OFF ) option( T8CODE_USE_SYSTEM_SC "Use system-installed sc library" OFF ) @@ -218,6 +221,13 @@ if( T8CODE_ENABLE_VTK ) endif (VTK_FOUND) endif( T8CODE_ENABLE_VTK ) +if( T8CODE_ENABLE_MRA ) + find_package(GSL REQUIRED) + if(GSL_FOUND) + message("Found GSL") + endif(GSL_FOUND) +endif( T8CODE_ENABLE_MRA ) + if( T8CODE_ENABLE_OCC ) find_package( OpenCASCADE REQUIRED COMPONENTS TKBO TKPrim TKTopAlgo diff --git a/cmake/thirdparty.cmake b/cmake/thirdparty.cmake index 34e5b13110..6a5c67b3aa 100644 --- a/cmake/thirdparty.cmake +++ b/cmake/thirdparty.cmake @@ -72,6 +72,22 @@ foreach(INDEX RANGE ${DEPS_RANGE}) message(WARNING "Unknown thirdparty library type '${DEP_TYPE}' for ${DEP_NAME}") endif() - # 3. Populate the thirdparty library - FetchContent_MakeAvailable(${DEP_NAME}) + # 3. Populate the thirdparty library. + # Build googletest statically even when t8code is a shared library: a shared + # libgtest.so collides at load time with a system libgtest.so of the same + # soname but different symbols (e.g. the DLR-SC MPI fork), causing runtime + # "undefined symbol" errors. A static gtest is baked into each test binary. + if(DEP_NAME STREQUAL "googletest") + set(_t8_saved_build_shared "${BUILD_SHARED_LIBS}") + set(_t8_saved_pic "${CMAKE_POSITION_INDEPENDENT_CODE}") + set(BUILD_SHARED_LIBS OFF) + set(CMAKE_POSITION_INDEPENDENT_CODE ON) + FetchContent_MakeAvailable(${DEP_NAME}) + set(BUILD_SHARED_LIBS "${_t8_saved_build_shared}") + set(CMAKE_POSITION_INDEPENDENT_CODE "${_t8_saved_pic}") + unset(_t8_saved_build_shared) + unset(_t8_saved_pic) + else() + FetchContent_MakeAvailable(${DEP_NAME}) + endif() endforeach() diff --git a/cmake/thirdparty.json b/cmake/thirdparty.json index f3e51f1df7..f7979c20c1 100644 --- a/cmake/thirdparty.json +++ b/cmake/thirdparty.json @@ -29,6 +29,16 @@ "ref": "2296a990d8b6b54731a63be0ba5bc17b08cd1f3d", "shallow": "TRUE" } + }, + { + "name": "unordered_dense", + "depends_on_cmake_option": "", + "source": { + "type": "git", + "url": "https://github.com/martinus/unordered_dense.git", + "ref": "v4.5.0", + "shallow": "TRUE" + } } ] } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 53a710f213..355a4e88fa 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -114,6 +114,109 @@ if( T8CODE_ENABLE_OCC ) ) endif() +if( T8CODE_ENABLE_MRA ) + target_compile_definitions( T8 PUBLIC T8_ENABLE_MRA ) + + # Add GSL library + target_link_libraries( T8 PUBLIC GSL::gsl GSL::gslcblas ) + + # Add unordered_dense library https://github.com/martinus/unordered_dense + # Fetched via cmake/thirdparty.json; unordered_dense_SOURCE_DIR is set by FetchContent. + set( UNORDERED_DENSE_DIR "${unordered_dense_SOURCE_DIR}/include" ) + + target_include_directories(T8 PUBLIC + $ + $ + $ + ) + + target_sources( T8 PRIVATE + t8_mra/num/basis/dubiner.cxx + t8_mra/num/quadrature/dunavant.cxx + ) + + install( FILES + t8_mra/t8_mra.hxx + DESTINATION include/t8_mra ) + + install( FILES + t8_mra/core/multiscale.hxx + t8_mra/core/forest_backend.hxx + t8_mra/core/mst.hxx + t8_mra/core/shape_traits.hxx + t8_mra/core/face_neighbor.hxx + DESTINATION include/t8_mra/core ) + + install( FILES + t8_mra/core/shape/mst_policy.hxx + t8_mra/core/shape/cartesian.hxx + t8_mra/core/shape/triangle.hxx + DESTINATION include/t8_mra/core/shape ) + + install( FILES + t8_mra/core/adapt/grading.hxx + t8_mra/core/adapt/coarsen.hxx + t8_mra/core/adapt/refine.hxx + t8_mra/core/adapt/balance.hxx + DESTINATION include/t8_mra/core/adapt ) + + install( FILES + t8_mra/io/vtk.hxx + t8_mra/io/dataset.hxx + DESTINATION include/t8_mra/io ) + + install( FILES + t8_mra/dg/dg_base.hxx + t8_mra/dg/cartesian.hxx + t8_mra/dg/triangle.hxx + DESTINATION include/t8_mra/dg ) + + install( FILES + t8_mra/criteria/coarsening_criterion.hxx + t8_mra/criteria/refinement_criterion.hxx + DESTINATION include/t8_mra/criteria ) + + install( FILES + t8_mra/data/element_data.hxx + t8_mra/data/levelindex_map.hxx + t8_mra/data/levelindex_set.hxx + t8_mra/data/levelmultiindex.hxx + t8_mra/data/triangle_order.hxx + DESTINATION include/t8_mra/data ) + + install( FILES + t8_mra/data/shape/cartesian.hxx + t8_mra/data/shape/triangle.hxx + DESTINATION include/t8_mra/data/shape ) + + install( FILES + t8_mra/num/cell_geometry.hxx + t8_mra/num/dg_basis.hxx + t8_mra/num/geometry.hxx + t8_mra/num/mask_coefficients.hxx + t8_mra/num/mat.hxx + t8_mra/num/nodal_to_modal.hxx + DESTINATION include/t8_mra/num ) + + install( FILES + t8_mra/num/shape/cartesian.hxx + t8_mra/num/shape/triangle.hxx + DESTINATION include/t8_mra/num/shape ) + + install( FILES + t8_mra/num/basis/basis.hxx + t8_mra/num/basis/legendre.hxx + t8_mra/num/basis/dubiner.hxx + DESTINATION include/t8_mra/num/basis ) + + install( FILES + t8_mra/num/quadrature/quadrature.hxx + t8_mra/num/quadrature/gauss_legendre.hxx + t8_mra/num/quadrature/dunavant.hxx + t8_mra/num/quadrature/dunavant_table.hxx + DESTINATION include/t8_mra/num/quadrature ) +endif() + if( T8CODE_BUILD_PEDANTIC ) target_compile_options( T8 PUBLIC -pedantic ) set (T8_CXXFLAGS "${T8_CXXFLAGS} -Wpedantic") diff --git a/src/t8_mra/core/adapt/balance.hxx b/src/t8_mra/core/adapt/balance.hxx new file mode 100644 index 0000000000..2c2e6a93ac --- /dev/null +++ b/src/t8_mra/core/adapt/balance.hxx @@ -0,0 +1,90 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include + +#include "sc_mpi.h" + +#include "t8.h" +#include "t8_eclass/t8_eclass.h" +#include "t8_element/t8_element.h" + +#include "t8_mra/core/adapt/grading.hxx" + +namespace t8_mra::adapt +{ + +/** + * @brief One balancing round: refine covering leaves across faces + * + * Every leaf resolves its face neighbours to their covering leaf; a covering + * leaf more than one level coarser is refined one level (children data = + * inverse two-scale with zero details, so the data is unchanged). + * + * @return Number of leaves marked in this round + */ +template +[[nodiscard]] unsigned int +balance_round (TMultiscale &mra) +{ + clear_state (mra); + + int mpirank; + int mpisize; + sc_MPI_Comm_rank (mra.grid.comm, &mpirank); + sc_MPI_Comm_size (mra.grid.comm, &mpisize); + std::vector> outgoing (mpisize); + + mra.grid.for_each_face_neigh ( + [] (const auto &lmi) { return lmi.level () >= 2; }, + [&] (const auto &, t8_eclass_t tree_class, t8_gloidx_t neigh_gtreeid, t8_element_t *neigh_element, + const auto &neigh_lmi) { + if (refine_covering_leaf (mra, neigh_lmi, 0, 1u, no_prior_marks {}) < 0 && mpisize > 1) { + const auto owner = mra.grid.find_owner (neigh_gtreeid, neigh_element, tree_class); + + if (owner != mpirank) + outgoing[owner].push_back (neigh_lmi.index); + } + }); + + if (mpisize > 1) + exchange_refine_requests (mra, outgoing, 0, 1u, no_prior_marks {}); + + const auto num_marked = mra.grid.global_num_marks (num_refinement_marks (mra, 0, mra.grid.maximum_level)); + + if (num_marked == 0) { + clear_state (mra); + return 0; + } + + apply_refinement (mra, 0, mra.grid.maximum_level, 0); + clear_state (mra); + + return num_marked; +} + +/** + * @brief Restore the 2:1 face balance of the grid + * + * Rounds iterate until no leaf has a face neighbour more than one level coarser; + * a jump of k levels resolves in k-1 rounds. Terminates: every round refines at + * least one leaf and levels are bounded by max_level. + */ +template +void +balance (TMultiscale &mra) +{ + auto rounds = 0; + + while (balance_round (mra) > 0) + t8_debugf ("MRA balance round %d\n", rounds++); + + if (rounds > 0) + mra.grid.repartition (); +} + +} // namespace t8_mra::adapt + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/core/adapt/coarsen.hxx b/src/t8_mra/core/adapt/coarsen.hxx new file mode 100644 index 0000000000..12aa680634 --- /dev/null +++ b/src/t8_mra/core/adapt/coarsen.hxx @@ -0,0 +1,304 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include +#include +#include +#include + +#include "sc_mpi.h" + +#include "t8.h" +#include "t8_cmesh/t8_cmesh.h" +#include "t8_eclass/t8_eclass.h" +#include "t8_element/t8_element.h" +#include "t8_forest/t8_forest_general.h" +#include "t8_schemes/t8_scheme.hxx" + +#include "t8_mra/core/adapt/grading.hxx" +#include "t8_mra/criteria/coarsening_criterion.hxx" +#include "t8_mra/data/element_data.hxx" +#include "t8_mra/data/levelmultiindex.hxx" + +namespace t8_mra::adapt +{ + +/** + * @brief Destructive fine->coarse sweep collapsing non-significant families + * + * Each complete family is two-scale transformed; a non-significant one is + * collapsed in place (parent replaces the children in lmi_map, children marked). + * Collapsing before the sweep descends makes the parent a leaf for the next + * level, so one traversal captures the whole cascade. + * + * @return Number of families collapsed on this rank + */ +template +[[nodiscard]] unsigned int +coarsen_sweep (TMultiscale &mra, int min_level, int max_level, TCriterion &criterion) +{ + using element_t = typename TMultiscale::element_t; + using detail_t = typename TMultiscale::detail_t; + using levelmultiindex = typename TMultiscale::levelmultiindex; + + auto *lmi_map = mra.get_lmi_map (); + auto num_marked = 0u; + + for (auto l = max_level; l > min_level; --l) { + typename TMultiscale::index_set candidates; + candidates.reserve (lmi_map->size (l)); + + for (const auto &[lmi, _] : (*lmi_map)[l]) + candidates.insert (t8_mra::parent_lmi (lmi)); + + for (const auto &parent : candidates) { + const auto siblings = t8_mra::children_lmi (parent); + + std::array data_on_siblings; + auto family_complete = true; + + for (auto k = 0u; k < levelmultiindex::NUM_CHILDREN; ++k) { + const auto *sibling = lmi_map->find (siblings[k]); + if (sibling == nullptr) { + family_complete = false; + break; + } + data_on_siblings[k] = *sibling; + } + + if (!family_complete) + continue; + + detail_t data_on_coarse; + mra.transform.two_scale_family (data_on_siblings, data_on_coarse); + mra.d_map.insert (parent, data_on_coarse); + + if (criterion.significant (mra, parent)) + continue; + + lmi_map->insert (parent, static_cast (data_on_coarse)); + for (const auto &child : siblings) { + lmi_map->erase (child); + mra.coarsening_set.insert (child); + } + + ++num_marked; + } + } + + return num_marked; +} + +/** + * @brief Adaptive coarsening from max_level down to min_level + * + * One destructive fine->coarse sweep on the maps per pass; across ranks an outer + * fixpoint (adapt + repartition make seam families whole) until no rank marks. + */ +template + requires coarsening_criterion +void +coarsen (TMultiscale &mra, int min_level, int max_level, TCriterion criterion = {}) +{ + if constexpr (criterion_has_prepare) + criterion.prepare (mra); + + for (auto pass = 0;; ++pass) { + clear_state (mra); + + const auto num_marked = coarsen_sweep (mra, min_level, max_level, criterion); + + t8_debugf ("MRA coarsen pass %d: %u families marked, %zu leaves remain\n", pass, num_marked, + mra.get_lmi_map ()->size ()); + + if (mra.grid.global_num_marks (num_marked) == 0) + break; + + mra.grid.adapt (TMultiscale::static_coarsening_callback, 1); + mra.grid.repartition (); + } + + clear_state (mra); +} + +/// Per-component max mean magnitude over a level's leaves, reduced across ranks +/// (floored at 1) so every rank normalizes jump detection identically. +template +[[nodiscard]] auto +global_v_max (TMultiscale &mra, int level) +{ + std::array local; + local.fill (1.0); + + for (const auto &[lmi, _] : (*mra.get_lmi_map ())[level]) { + const auto m = mra.mean_val (lmi); + + for (auto u = 0u; u < TMultiscale::U_DIM; ++u) + local[u] = std::max (local[u], std::abs (m[u])); + } + + std::array global; + sc_MPI_Allreduce (local.data (), global.data (), TMultiscale::U_DIM, sc_MPI_DOUBLE, sc_MPI_MAX, mra.grid.comm); + + return global; +} + +/** + * @brief Mean-value jump detection on the leaves of one level + * + * Marks a family when a face-neighbour mean difference exceeds c_thresh*sqrt(h): + * smooth data decays as O(h) and falls below, a discontinuity stays O(1). Remote + * neighbours come from the ghost layer; the result is globalized because coarsen + * repartitions between passes. + * + * @return Parent lmis of the jumping families + */ +template +[[nodiscard]] auto +detect_jumps (TMultiscale &mra, int level, double c_thresh) +{ + using levelmultiindex = typename TMultiscale::levelmultiindex; + + mra.grid.ghost_exchange (); + + auto *lmi_map = mra.get_lmi_map (); + auto &ghost_map = mra.grid.ghost_map; + const auto v_max = global_v_max (mra, level); + + std::unordered_map face_jump; + mra.grid.for_each_face_neigh ([&] (const auto &lmi) { return lmi.level () == static_cast (level); }, + [&] (const auto &lmi, t8_eclass_t, t8_gloidx_t, t8_element_t *, const auto &neigh_lmi) { + const auto *neigh_data = lmi_map->contains (neigh_lmi) ? &lmi_map->get (neigh_lmi) + : ghost_map.contains (neigh_lmi) ? &ghost_map.get (neigh_lmi) + : nullptr; + if (neigh_data == nullptr) + return; + + const auto mean_inner = mra.mean_val (lmi_map->get (lmi)); + const auto mean_neigh = mra.mean_val (*neigh_data); + auto &diff = face_jump[lmi.index]; + + for (auto u = 0u; u < TMultiscale::U_DIM; ++u) + diff = std::max (diff, std::abs (mean_inner[u] - mean_neigh[u]) / v_max[u]); + }); + + typename TMultiscale::index_set jumps; + for (const auto &[index, diff] : face_jump) { + const auto lmi = levelmultiindex (index); + const auto h = std::pow (lmi_map->get (lmi).vol, 1.0 / TMultiscale::DIM); + + if (diff > c_thresh * std::sqrt (h)) + jumps.insert (t8_mra::parent_lmi (lmi)); + } + + mra.grid.globalize (jumps); + + return jumps; +} + +/// Coarsening criterion wrapper: families with a detected jump are always +/// significant. +template +struct jump_guarded +{ + TCriterion &criterion; + const typename TMultiscale::index_set &jumps; + + void + prepare (TMultiscale &mra) + { + if constexpr (criterion_has_prepare) + criterion.prepare (mra); + } + + bool + significant (TMultiscale &mra, const typename TMultiscale::levelmultiindex &lmi) + { + return jumps.contains (lmi) || criterion.significant (mra, lmi); + } +}; + +/** + * @brief Refine every leaf at the given level and project the initial data + * + * Unlike refine(), the children data is projected directly from the initial + * data (exact up to quadrature), not predicted. + * + * @return Number of leaves refined + */ +template +unsigned int +project_onto_children (TMultiscale &mra, int level, TFunc &&func) +{ + clear_state (mra); + + for (const auto &[lmi, _] : (*mra.get_lmi_map ())[level]) + mra.refinement_set.insert (lmi); + + const auto num_marked = mra.refinement_set[level].size (); + if (mra.grid.global_num_marks (num_marked) == 0) + return 0; + + mra.grid.adapt (TMultiscale::static_refinement_callback); + + auto *lmi_map = mra.get_lmi_map (); + auto *user_data = mra.get_user_data (); + + const auto num_local_trees = t8_forest_get_num_local_trees (mra.grid.get_forest ()); + auto current_idx = 0; + + for (auto tree_idx = 0; tree_idx < num_local_trees; ++tree_idx) { + const auto num_elements = t8_forest_get_tree_num_leaf_elements (mra.grid.get_forest (), tree_idx); + + for (auto ele_idx = 0; ele_idx < num_elements; ++ele_idx, ++current_idx) { + const auto lmi = t8_mra::get_lmi_from_forest_data (user_data, current_idx); + + if (lmi_map->contains (lmi)) + continue; + + const auto *element = t8_forest_get_leaf_element_in_tree (mra.grid.get_forest (), tree_idx, ele_idx); + lmi_map->insert (lmi, mra.project_leaf (tree_idx, element, func)); + } + } + + for (const auto &lmi : mra.refinement_set[level]) + lmi_map->erase (lmi); + + clear_state (mra); + + return num_marked; +} + +/** + * @brief Adaptive bottom-up initialization on given initial data + * + * Projects onto the uniform level-1 forest, then per level thresholds the + * details and refines the significant leaves one further level by direct + * projection; jumping families are kept regardless. Never builds the uniform + * max_level grid. + */ +template + requires coarsening_criterion +void +initialize_data_adaptive (TMultiscale &mra, t8_cmesh_t mesh, const t8_scheme *scheme, int max_level, TFunc &&func, + TCriterion criterion = {}) +{ + auto c_thresh = 1.0; + if constexpr (requires { criterion.c_thresh; }) + c_thresh = criterion.c_thresh; + + mra.initialize_data (mesh, scheme, 1, func); + + for (auto l = 1; l < max_level; ++l) { + const auto jumps = detect_jumps (mra, l, c_thresh); + + coarsen (mra, std::max (l - 1, 1), l, jump_guarded { criterion, jumps }); + project_onto_children (mra, l, func); + } +} + +} // namespace t8_mra::adapt + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/core/adapt/grading.hxx b/src/t8_mra/core/adapt/grading.hxx new file mode 100644 index 0000000000..766a3cbfd4 --- /dev/null +++ b/src/t8_mra/core/adapt/grading.hxx @@ -0,0 +1,217 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include +#include +#include + +#include "sc_mpi.h" + +#include "t8.h" +#include "t8_eclass/t8_eclass.h" +#include "t8_element/t8_element.h" + +#include "t8_mra/data/levelmultiindex.hxx" + +namespace t8_mra::adapt +{ + +/// Reset all per-pass multiscale state. +template +void +clear_state (TMultiscale &mra) +{ + mra.d_map.erase_all (); + mra.td_set.erase_all (); + mra.refinement_set.erase_all (); + mra.coarsening_set.erase_all (); +} + +/// Number of leaves marked for refinement in [min_level, max_level). +template +[[nodiscard]] unsigned int +num_refinement_marks (TMultiscale &mra, int min_level, int max_level) +{ + auto num = 0u; + + for (auto l = min_level; l < max_level; ++l) + num += mra.refinement_set[l].size (); + + return num; +} + +/// Reconstruct children data (inverse two-scale, zero details) for the marks in +/// refinement_set, then realize them with one forest adapt. +template +void +apply_refinement (TMultiscale &mra, int min_level, int max_level, int recursive) +{ + mra.d_map.erase_all (); + for (auto l = min_level; l < max_level; ++l) + for (const auto &lmi : mra.refinement_set[l]) + mra.d_map.insert (lmi, typename TMultiscale::detail_t {}); + + mra.inverse_multiscale_transformation (min_level, max_level); + mra.grid.adapt (TMultiscale::static_refinement_callback, recursive); +} + +/// Stand-in for callers that realize their marks each round (balance) and never +/// descend a prior-refinements path. +struct no_prior_marks +{ + template + bool + contains (const TLmi & /*unused*/) const + { + return false; + } +}; + +/** + * @brief Find the covering leaf of a same-level neighbour and refine it + * + * Walks up to the covering leaf, then descends towards the neighbour through + * prior_refinements (marks treated as performed). A covering leaf more than + * max_level_gap levels coarser than the neighbour is refined one level (0: + * grading, exact match; 1: 2:1 balance). + * + * @return 1 on a new mark, 0 if nothing to do, -1 if no covering leaf is local + */ +template +[[nodiscard]] int +refine_covering_leaf (TMultiscale &mra, const TLmi &neigh_lmi, int min_level, unsigned int max_level_gap, + const TPriorRefinements &prior_refinements) +{ + auto *lmi_map = mra.get_lmi_map (); + + auto walk = neigh_lmi; + while (walk.level () > 0 && !lmi_map->contains (walk)) + walk = t8_mra::parent_lmi (walk); + + if (!lmi_map->contains (walk)) + return -1; + + while (walk.level () + max_level_gap < neigh_lmi.level () && prior_refinements.contains (walk)) { + auto down = neigh_lmi; + + while (down.level () > walk.level () + 1) + down = t8_mra::parent_lmi (down); + walk = down; + } + + if (walk.level () + max_level_gap < neigh_lmi.level () && static_cast (walk.level ()) >= min_level + && !mra.refinement_set.contains (walk)) { + mra.refinement_set.insert (walk); + + return 1; + } + + return 0; +} + +/** + * @brief Ship pull-up requests to their owner ranks, resolve received ones + * + * A same-level neighbour whose covering leaf is not local is sent to the rank + * owning that region; the owner resolves it against its own lmi_map and marks + * its own refinement_set. Collective. + * + * @return Number of new LOCAL marks created by received requests + */ +template +unsigned int +exchange_refine_requests (TMultiscale &mra, const std::vector> &outgoing, int min_level, + unsigned int max_level_gap, const TPriorRefinements &prior_refinements) +{ + using levelmultiindex = typename TMultiscale::levelmultiindex; + + int mpisize; + sc_MPI_Comm_size (mra.grid.comm, &mpisize); + + std::vector send_counts; + send_counts.reserve (mpisize); + + std::ranges::transform (outgoing, std::back_inserter (send_counts), + [] (const auto &list) { return static_cast (list.size ()); }); + + std::vector recv_counts (mpisize, 0); + sc_MPI_Alltoall (send_counts.data (), 1, sc_MPI_INT, recv_counts.data (), 1, sc_MPI_INT, mra.grid.comm); + + std::vector> incoming (mpisize); + std::vector requests; + requests.reserve (2 * mpisize); + + for (auto rank = 0; rank < mpisize; ++rank) { + if (recv_counts[rank] > 0) { + incoming[rank].resize (recv_counts[rank]); + requests.emplace_back (); + + sc_MPI_Irecv (incoming[rank].data (), recv_counts[rank] * sizeof (size_t), sc_MPI_BYTE, rank, 0, mra.grid.comm, + &requests.back ()); + } + + if (send_counts[rank] > 0) { + requests.emplace_back (); + sc_MPI_Isend (const_cast (outgoing[rank].data ()), send_counts[rank] * sizeof (size_t), sc_MPI_BYTE, + rank, 0, mra.grid.comm, &requests.back ()); + } + } + sc_MPI_Waitall (static_cast (requests.size ()), requests.data (), sc_MPI_STATUSES_IGNORE); + + auto num_new_marks = 0u; + for (const auto &batch : incoming) + num_new_marks += static_cast (std::ranges::count_if (batch, [&] (const auto &idx) { + return refine_covering_leaf (mra, levelmultiindex (idx), min_level, max_level_gap, prior_refinements) > 0; + })); + + return num_new_marks; +} + +/** + * @brief One grading round: same-level neighbours of marked families + * + * Every leaf of a family in td_set refines its coarser covering leaves by one + * level; larger jumps resolve over repeated rounds against prior_refinements. A + * neighbour whose covering leaf is remote is shipped to its owner. Collective: + * the returned count is global. + * + * @return Global number of new marks in this round + */ +template +[[nodiscard]] unsigned int +neighbour_prediction (TMultiscale &mra, int min_level, const TPriorRefinements &prior_refinements) +{ + int mpirank; + int mpisize; + sc_MPI_Comm_rank (mra.grid.comm, &mpirank); + sc_MPI_Comm_size (mra.grid.comm, &mpisize); + + std::vector> outgoing (mpisize); + auto num_new_marks = 0u; + + mra.grid.for_each_face_neigh ( + [&] (const auto &lmi) { return lmi.level () != 0 && mra.td_set.contains (t8_mra::parent_lmi (lmi)); }, + [&] (const auto &, t8_eclass_t tree_class, t8_gloidx_t neigh_gtreeid, t8_element_t *neigh_element, + const auto &neigh_lmi) { + const auto res = refine_covering_leaf (mra, neigh_lmi, min_level, 0u, prior_refinements); + if (res > 0) + ++num_new_marks; + else if (res < 0 && mpisize > 1) { + const auto owner = mra.grid.find_owner (neigh_gtreeid, neigh_element, tree_class); + + if (owner != mpirank) + outgoing[owner].push_back (neigh_lmi.index); + } + }); + + if (mpisize > 1) + num_new_marks += exchange_refine_requests (mra, outgoing, min_level, 0u, prior_refinements); + + return mra.grid.global_num_marks (num_new_marks); +} + +} // namespace t8_mra::adapt + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/core/adapt/refine.hxx b/src/t8_mra/core/adapt/refine.hxx new file mode 100644 index 0000000000..e1b9689330 --- /dev/null +++ b/src/t8_mra/core/adapt/refine.hxx @@ -0,0 +1,99 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include + +#include + +#include "t8_mra/core/adapt/grading.hxx" +#include "t8_mra/criteria/coarsening_criterion.hxx" +#include "t8_mra/criteria/refinement_criterion.hxx" +#include "t8_mra/data/levelmultiindex.hxx" + +namespace t8_mra::adapt +{ + +/** + * @brief Adaptive refinement from min_level up to max_level + * + * The non-destructive transform yields every family's details; the criterion + * grades neighbourhoods (td_set) and refines steep families' children. One pass + * suffices (new children carry zero details); a grading fixpoint then pulls + * covering leaves up one level per round. All marks are realized by one recursive + * forest adapt (children data = inverse two-scale with zero details). + */ +template + requires refinement_criterion +void +refine (TMultiscale &mra, int min_level, int max_level, TCriterion criterion = {}) +{ + if constexpr (criterion_has_prepare) + criterion.prepare (mra); + + clear_state (mra); + + mra.multiscale_transformation (0, max_level); + + auto num_families = 0u; + for (auto l = 0; l < max_level; ++l) { + for (const auto &[lmi, _] : mra.d_map[l]) { + ++num_families; + + const auto flags = criterion (mra, lmi); + + if (flags.grade_neighbours) + mra.td_set.insert (lmi); + + if (l < max_level - 1 && flags.refine_children) + for (const auto &child : t8_mra::children_lmi (lmi)) + mra.refinement_set.insert (child); + } + } + + for (auto l = 0; l < min_level; ++l) + mra.refinement_set.erase (l); + + auto prior_refinements = mra.refinement_set; + prior_refinements.erase_all (); + + for (auto round = 0;; ++round) { + const auto new_marks = neighbour_prediction (mra, min_level, prior_refinements); + + t8_debugf ("MRA refine grading round %d: %u new marks\n", round, new_marks); + if (new_marks == 0) + break; + + prior_refinements = mra.refinement_set; + + typename TMultiscale::index_set stopped; + for (auto l = 0; l < max_level; ++l) + for (const auto &lmi : mra.td_set[l]) { + const auto children = t8_mra::children_lmi (lmi); + + if (std::any_of (children.begin (), children.end (), + [&] (const auto &child) { return mra.refinement_set.contains (child); })) + stopped.insert (lmi); + } + + for (const auto &lmi : stopped) + mra.td_set.erase (lmi); + } + + const auto num_marked = num_refinement_marks (mra, min_level, max_level); + t8_debugf ("MRA refine analysis: %u leaf families, %u leaves marked\n", num_families, num_marked); + + if (mra.grid.global_num_marks (num_marked) == 0) { + clear_state (mra); + return; + } + + apply_refinement (mra, min_level, max_level, 1); + mra.grid.repartition (); + + clear_state (mra); +} + +} // namespace t8_mra::adapt + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/core/face_neighbor.hxx b/src/t8_mra/core/face_neighbor.hxx new file mode 100644 index 0000000000..3030003d34 --- /dev/null +++ b/src/t8_mra/core/face_neighbor.hxx @@ -0,0 +1,58 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include +#include +#include +#include + +#include "t8_mra/data/element_data.hxx" +#include "t8_mra/data/levelindex_map.hxx" +#include "t8_mra/data/levelmultiindex.hxx" + +namespace t8_mra +{ + +/// lmi -> forest-local leaf index, the reverse of forest_data::lmi_idx. +template +using local_index_map = levelindex_map, t8_locidx_t>; + +/// Reverse of lmi_idx (lmi -> local index); rebuild on grid change. +template +[[nodiscard]] local_index_map +build_local_index_map (const forest_data *forest_data, t8_locidx_t num_local, unsigned int max_level) +{ + local_index_map reverse_map (max_level); + + for (auto i = 0; i < num_local; ++i) + reverse_map.insert (get_lmi_from_forest_data (forest_data, i), i); + + return reverse_map; +} + +/// Local index of the same-level leaf across `face`; -1 at a boundary or non- +/// conforming face. +template +[[nodiscard]] t8_locidx_t +face_neighbor_index (t8_forest_t forest, t8_locidx_t tree_idx, const t8_element_t *element, int face, + t8_element_t *element_buffer, const t8_scheme *scheme, + const levelindex_map &reverse_map, int *neigh_face) +{ + const auto tree_class = t8_forest_get_tree_class (forest, tree_idx); + const auto neigh_gtreeid + = t8_forest_element_face_neighbor (forest, tree_idx, element, element_buffer, tree_class, face, neigh_face); + + if (neigh_gtreeid < 0) + return -1; + + const TLmi neigh_lmi (neigh_gtreeid, element_buffer, scheme); + const t8_locidx_t *idx = reverse_map.find (neigh_lmi); + + return idx ? *idx : -1; +} + +} // namespace t8_mra + +#endif diff --git a/src/t8_mra/core/forest_backend.hxx b/src/t8_mra/core/forest_backend.hxx new file mode 100644 index 0000000000..8fa69e8724 --- /dev/null +++ b/src/t8_mra/core/forest_backend.hxx @@ -0,0 +1,425 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sc_containers.h" +#include "sc_mpi.h" + +#include "t8.h" +#include "t8_eclass/t8_eclass.h" +#include "t8_element/t8_element.h" +#include "t8_forest/t8_forest_general.h" +#include "t8_forest/t8_forest_geometrical.h" +#include "t8_forest/t8_forest_ghost.h" +#include "t8_forest/t8_forest_partition.h" +#include "t8_forest/t8_forest_types.h" + +#include "t8_mra/data/element_data.hxx" +#include "t8_mra/data/levelindex_map.hxx" +#include "t8_mra/data/levelmultiindex.hxx" + +namespace t8_mra +{ + +/** + * @brief The t8code + MPI side of a multiscale instance. + * + * Owns the forest, its user data (lmi_map + lmi_idx) and the ghost snapshot. + * The interface to the mst side is the lmi_map. + * + * @tparam TShape element shape, @tparam U components, @tparam P order + */ +template +class forest_backend { + public: + using element_t = element_data; + using levelmultiindex = t8_mra::levelmultiindex; + using lmi_map_t = levelindex_map; + using user_data_t = t8_mra::forest_data; + + t8_forest_t forest = nullptr; + sc_MPI_Comm comm; + unsigned int maximum_level; + lmi_map_t ghost_map; + + forest_backend (int _max_level, sc_MPI_Comm _comm) + : comm (_comm), maximum_level (static_cast (_max_level)), ghost_map (_max_level) + { + } + + /** @brief Set the multiscale instance and the post-adaptation hook. */ + void + bind (void *instance, std::function post_adapt_hook) + { + mra_instance = instance; + post_adapt = std::move (post_adapt_hook); + } + + [[nodiscard]] t8_forest_t + get_forest () const + { + return forest; + } + + [[nodiscard]] user_data_t * + get_user_data () const + { + return reinterpret_cast (t8_forest_get_user_data (forest)); + } + + [[nodiscard]] lmi_map_t * + get_lmi_map () const + { + return get_user_data ()->lmi_map; + } + + /** @brief Local leaves in SFC order. func: (tree_idx, element, local leaf idx, global tree id). */ + template + void + for_each_local_leaf (TFunc &&func) const + { + const auto num_local_trees = t8_forest_get_num_local_trees (forest); + auto local_idx = 0u; + + for (auto tree_idx = 0; tree_idx < num_local_trees; ++tree_idx) { + const auto num_elems = t8_forest_get_tree_num_leaf_elements (forest, tree_idx); + const auto global_tree = t8_forest_global_tree_id (forest, tree_idx); + + for (auto ele_idx = 0; ele_idx < num_elems; ++ele_idx, ++local_idx) { + const auto *element = t8_forest_get_leaf_element_in_tree (forest, tree_idx, ele_idx); + func (tree_idx, element, local_idx, global_tree); + } + } + } + + /** + * @brief Same-level face neighbours of the leaves passing leaf_filter. + * + * @param leaf_filter gate on the source leaf lmi + * @param func (source lmi, neigh tree class, neigh gtree, neigh element scratch, neigh lmi) + */ + template + void + for_each_face_neigh (TLeafFilter &&leaf_filter, TFunc &&func) const + { + auto *user_data = get_user_data (); + const auto *scheme = t8_forest_get_scheme (forest); + + const auto num_local_trees = t8_forest_get_num_local_trees (forest); + auto current_idx = 0u; + + for (auto tree_idx = 0; tree_idx < num_local_trees; ++tree_idx) { + const auto tree_class = t8_forest_get_tree_class (forest, tree_idx); + const auto num_elements = t8_forest_get_tree_num_leaf_elements (forest, tree_idx); + + t8_element_t *neigh_element; + scheme->element_new (tree_class, 1, &neigh_element); + + for (auto ele_idx = 0; ele_idx < num_elements; ++ele_idx, ++current_idx) { + const auto lmi = t8_mra::get_lmi_from_forest_data (user_data, current_idx); + if (!leaf_filter (lmi)) + continue; + + const auto *element = t8_forest_get_leaf_element_in_tree (forest, tree_idx, ele_idx); + const auto num_faces = scheme->element_get_num_faces (tree_class, element); + + for (auto face = 0; face < num_faces; ++face) { + int neigh_face; + const auto neigh_gtreeid + = t8_forest_element_face_neighbor (forest, tree_idx, element, neigh_element, tree_class, face, &neigh_face); + + if (neigh_gtreeid < 0) + continue; + + func (lmi, tree_class, neigh_gtreeid, neigh_element, levelmultiindex (neigh_gtreeid, neigh_element, scheme)); + } + } + + scheme->element_destroy (tree_class, 1, &neigh_element); + } + } + + [[nodiscard]] int + find_owner (t8_gloidx_t neigh_gtreeid, t8_element_t *neigh_element, t8_eclass_t tree_class) const + { + return t8_forest_element_find_owner (forest, neigh_gtreeid, neigh_element, tree_class); + } + + /** @brief Corner coordinates of a leaf in t8code vertex order. */ + void + element_corner_coords (t8_locidx_t tree_idx, const t8_element_t *element, + std::array, T8_ECLASS_MAX_CORNERS> &corners) const + { + const auto num_corners = t8_eclass_num_vertices[TShape]; + for (auto corner = 0; corner < num_corners; ++corner) + t8_forest_element_coordinate (forest, tree_idx, element, corner, corners[corner].data ()); + } + + [[nodiscard]] double + element_volume (t8_locidx_t tree_idx, const t8_element_t *element) const + { + return t8_forest_element_volume (forest, tree_idx, element); + } + + /** @brief Fresh user data wrapping map, lmi_idx sized to local+ghost. */ + [[nodiscard]] user_data_t * + attach_user_data (t8_forest_t f, lmi_map_t *lmi_map) + { + auto *user_data = T8_ALLOC (user_data_t, 1); + user_data->lmi_map = lmi_map; + + const auto num_local = t8_forest_get_local_num_leaf_elements (f); + const auto num_ghost = t8_forest_get_num_ghosts (f); + + user_data->lmi_idx = sc_array_new_count (sizeof (levelmultiindex), num_local + num_ghost); + user_data->mra_instance = mra_instance; + t8_forest_set_user_data (f, user_data); + + return user_data; + } + + static void + destroy_user_data (user_data_t *user_data) + { + delete user_data->lmi_map; + + if (user_data->lmi_idx) + sc_array_destroy (user_data->lmi_idx); + + T8_FREE (user_data); + } + + /** + * @brief Rebuild lmi_idx from the committed leaves; the one per-leaf resync. + * + * @param per_leaf (tree_idx, element, lmi, leaf idx) per leaf in SFC order + */ + template + void + rebuild_leaf_index (t8_forest_t f, user_data_t *user_data, TPerLeaf &&per_leaf) + { + const auto *scheme = t8_forest_get_scheme (f); + const auto num_local_trees = t8_forest_get_num_local_trees (f); + t8_locidx_t current_idx = 0; + + for (t8_locidx_t tree_idx = 0; tree_idx < num_local_trees; ++tree_idx) { + const auto gtreeid = t8_forest_global_tree_id (f, tree_idx); + const auto num_elements = t8_forest_get_tree_num_leaf_elements (f, tree_idx); + + for (t8_locidx_t ele_idx = 0; ele_idx < num_elements; ++ele_idx, ++current_idx) { + const auto *element = t8_forest_get_leaf_element_in_tree (f, tree_idx, ele_idx); + const auto lmi = levelmultiindex (gtreeid, element, scheme); + t8_mra::set_lmi_forest_data (user_data, current_idx, lmi); + + per_leaf (tree_idx, element, lmi, current_idx); + } + } + } + + /** @brief Build lmi_map + lmi_idx from projector: (tree_idx, element) -> element_t. */ + template + void + build (TProjector &&projector) + { + T8_ASSERT (t8_forest_is_committed (forest)); + + auto *map = new lmi_map_t (maximum_level); + auto *user_data = attach_user_data (forest, map); + + rebuild_leaf_index (forest, user_data, + [&] (t8_locidx_t tree_idx, const t8_element_t *element, const levelmultiindex &lmi, + t8_locidx_t) { map->insert (lmi, projector (tree_idx, element)); }); + } + + /** @brief One new_adapt pass; lmi_map is already current, only lmi_idx is rebuilt. */ + void + adapt (t8_forest_adapt_t adapt_callback, int recursive = 0) + { + t8_forest_ref (forest); + auto *old_user_data = get_user_data (); + t8_forest_t new_forest = t8_forest_new_adapt (forest, adapt_callback, recursive, 0, old_user_data); + + lmi_map_t *map = old_user_data->lmi_map; + old_user_data->lmi_map = new lmi_map_t (maximum_level); // placeholder freed with old_user_data + + auto *user_data = attach_user_data (new_forest, map); + rebuild_leaf_index (new_forest, user_data, + [] (t8_locidx_t, const t8_element_t *, const levelmultiindex &, t8_locidx_t) {}); + + destroy_user_data (old_user_data); + t8_forest_unref (&forest); + + forest = new_forest; + ghost_map.erase_all (); + + if (post_adapt) + post_adapt (); + } + + /** @brief Repartition (set_for_coarsening) and migrate leaf data; rebuild lmi_map + lmi_idx. */ + void + repartition () + { + static_assert (std::is_trivially_copyable_v, "element data is shipped as raw bytes"); + + int mpisize; + sc_MPI_Comm_size (comm, &mpisize); + if (mpisize == 1) + return; + + auto *old_user_data = get_user_data (); + auto *old_map = old_user_data->lmi_map; + + const auto num_old = t8_forest_get_local_num_leaf_elements (forest); + auto *data_in = sc_array_new_count (sizeof (element_t), num_old); + + for (auto i = 0; i < num_old; ++i) + *reinterpret_cast (sc_array_index (data_in, i)) + = old_map->get (t8_mra::get_lmi_from_forest_data (old_user_data, i)); + + t8_forest_ref (forest); + t8_forest_t new_forest; + + t8_forest_init (&new_forest); + t8_forest_set_partition (new_forest, forest, 1); + t8_forest_commit (new_forest); + + const auto num_new = t8_forest_get_local_num_leaf_elements (new_forest); + auto *data_out = sc_array_new_count (sizeof (element_t), num_new); + + t8_forest_partition_data (forest, new_forest, data_in, data_out); + sc_array_destroy (data_in); + + auto *map = new lmi_map_t (maximum_level); + auto *user_data = attach_user_data (new_forest, map); + + rebuild_leaf_index (new_forest, user_data, + [&] (t8_locidx_t, const t8_element_t *, const levelmultiindex &lmi, t8_locidx_t idx) { + map->insert (lmi, *reinterpret_cast (sc_array_index (data_out, idx))); + }); + + sc_array_destroy (data_out); + + destroy_user_data (old_user_data); + t8_forest_unref (&forest); + + forest = new_forest; + ghost_map.erase_all (); + + if (post_adapt) + post_adapt (); + } + + /** @brief Build the face-ghost layer and fill ghost_map with the remote leaves. Collective. */ + void + ghost_exchange () + { + ghost_map.erase_all (); + + int mpisize = 1; + sc_MPI_Comm_size (comm, &mpisize); + if (mpisize == 1) + return; + + if (forest->ghosts == nullptr) { + forest->ghost_type = T8_GHOST_FACES; + t8_forest_ghost_create_topdown (forest); + } + + const auto num_local = t8_forest_get_local_num_leaf_elements (forest); + const auto num_ghosts = t8_forest_get_num_ghosts (forest); + + auto *user_data = get_user_data (); + sc_array_resize (user_data->lmi_idx, num_local + num_ghosts); + t8_forest_ghost_exchange_data (forest, user_data->lmi_idx); + + auto *data = sc_array_new_count (sizeof (element_t), num_local + num_ghosts); + auto *lmi_map = get_lmi_map (); + + for (auto i = 0; i < num_local; ++i) + *reinterpret_cast (sc_array_index (data, i)) + = lmi_map->get (t8_mra::get_lmi_from_forest_data (user_data, i)); + + t8_forest_ghost_exchange_data (forest, data); + + for (auto i = num_local; i < num_local + num_ghosts; ++i) + ghost_map.insert (t8_mra::get_lmi_from_forest_data (user_data, i), + *reinterpret_cast (sc_array_index (data, i))); + + sc_array_destroy (data); + } + + /** @brief Allreduce-MAX of the local mark count. */ + [[nodiscard]] unsigned int + global_num_marks (unsigned int local_marks) const + { + auto global_marks = local_marks; + sc_MPI_Allreduce (&local_marks, &global_marks, 1, sc_MPI_UNSIGNED, sc_MPI_MAX, comm); + + return global_marks; + } + + /** @brief Replace a rank-local index set with its union over all ranks. Collective. */ + template + void + globalize (TIndexSet &set) const + { + int mpisize = 1; + sc_MPI_Comm_size (comm, &mpisize); + if (mpisize == 1) + return; + + std::vector local; + local.reserve (set.size ()); + std::ranges::transform (set, std::back_inserter (local), + [] (const auto &lmi) { return static_cast (lmi.index); }); + + int num_local = static_cast (local.size ()); + std::vector counts (mpisize); + std::vector displs (mpisize); + + sc_MPI_Allgather (&num_local, 1, sc_MPI_INT, counts.data (), 1, sc_MPI_INT, comm); + + std::exclusive_scan (counts.begin (), counts.end (), displs.begin (), 0); + const auto total = displs.back () + counts.back (); + + std::vector all (total); + + sc_MPI_Allgatherv (local.data (), num_local, T8_MPI_GLOIDX, all.data (), counts.data (), displs.data (), + T8_MPI_GLOIDX, comm); + + for (const auto idx : all) + set.insert (levelmultiindex (static_cast (idx))); + } + + void + cleanup () + { + ghost_map.erase_all (); + + if (forest != nullptr) { + if (auto *user_data = get_user_data ()) + destroy_user_data (user_data); + + t8_forest_unref (&forest); + forest = nullptr; + } + } + + private: + void *mra_instance = nullptr; + std::function post_adapt; +}; + +} // namespace t8_mra + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/core/mst.hxx b/src/t8_mra/core/mst.hxx new file mode 100644 index 0000000000..59dc71bbe5 --- /dev/null +++ b/src/t8_mra/core/mst.hxx @@ -0,0 +1,341 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include +#include + +#include "ankerl/unordered_dense.h" + +#include "t8_mra/core/shape/mst_policy.hxx" +#include "t8_mra/data/element_data.hxx" +#include "t8_mra/data/levelindex_map.hxx" +#include "t8_mra/data/levelmultiindex.hxx" +#include "t8_mra/num/mask_coefficients.hxx" +#include "t8_mra/num/mat.hxx" + +namespace t8_mra +{ + +/** + * @brief Two-scale (multiscale) transform operations on a levelindex_map. + * + * - two_scale_family: per-family kernel (children -> parent + details). + * - multiscale_transformation: forward, non-destructive; details to d_map. + * - multiscale_decomposition: forward, destructive; collapse to l_min. + * - inverse_multiscale_transformation: reconstruct children from details. + * + * Element-specific behaviour is routed through the ordering and scaling policies. + */ +template , + typename TOrderingPolicy = ordering_policy, + typename TScalingPolicy = mst_scaling_policy> +class mst { + public: + using element_t = TElement; + using detail_t = TDetail; + using levelmultiindex = t8_mra::levelmultiindex; + using index_set = ankerl::unordered_dense::set; + + static constexpr auto Shape = TElement::Shape; + static constexpr unsigned int U_DIM = TElement::U_DIM; + static constexpr unsigned int DOF = TElement::DOF; + + /// Two-scale mask coefficients, computed once from the reference basis. + std::vector mask; + + mst () + { + t8_mra::compute_mask (mask); + } + + void + two_scale_family (const std::array &data_on_siblings, + detail_t &data_on_coarse) const + { + two_scale_family (data_on_siblings, data_on_coarse, mask); + } + + void + multiscale_transformation (unsigned int l_min, unsigned int l_max, + levelindex_map &lmi_map, + levelindex_map &d_map) const + { + multiscale_transformation (l_min, l_max, lmi_map, d_map, mask); + } + + void + multiscale_decomposition (unsigned int l_min, unsigned int l_max, levelindex_map &lmi_map, + levelindex_map &d_map) const + { + multiscale_decomposition (l_min, l_max, lmi_map, d_map, mask); + } + + void + inverse_multiscale_transformation (unsigned int l_min, unsigned int l_max, + levelindex_map &lmi_map, + levelindex_map &d_map) const + { + inverse_multiscale_transformation (l_min, l_max, lmi_map, d_map, mask); + } + + /** + * @brief Two-scale transform of one complete family (children -> parent + details). + * + * u_parent[i] = scaling * Σ_k Σ_j u_child[k][j] * M[k](j,i) + * d[k][i] = u_child[k][i] - Σ_j M[k](i,j) * u_parent[j] + * + * @param data_on_siblings The NUM_CHILDREN children of the family. + * @param data_on_coarse Output parent: u_coeffs, d_coeffs, vol, order. + * @param mask_coefficients Two-scale mask matrices M[k]. + */ + static void + two_scale_family (const std::array &data_on_siblings, + detail_t &data_on_coarse, const std::vector &mask_coefficients) + { + const double scaling_factor = TScalingPolicy::forward_scaling_factor (levelmultiindex::NUM_CHILDREN); + + for (auto u = 0u; u < U_DIM; ++u) { + std::array u_parent; + + // Parent coefficients: u_parent[i] = scaling * Σ_k Σ_j u_child[k][j] * M[k](j,i) + for (auto i = 0u; i < DOF; ++i) { + auto sum = 0.0; + + for (auto k = 0u; k < levelmultiindex::NUM_CHILDREN; ++k) { + const auto &Mk = mask_coefficients[k]; + const auto &uk = data_on_siblings[k].u_coeffs; + + for (auto j = 0u; j < DOF; ++j) + sum += uk[element_t::dg_idx (u, j)] * Mk (j, i); + } + + u_parent[i] = sum * scaling_factor; + data_on_coarse.u_coeffs[element_t::dg_idx (u, i)] = u_parent[i]; + } + + // Detail coefficients: d[k][i] = u_child[k][i] - Σ_j M[k](i,j) * u_parent[j] + for (auto k = 0u; k < levelmultiindex::NUM_CHILDREN; ++k) { + const auto &Mk = mask_coefficients[k]; + const auto &uk = data_on_siblings[k].u_coeffs; + + for (auto i = 0u; i < DOF; ++i) { + auto sum = 0.0; + for (auto j = 0u; j < DOF; ++j) + sum += Mk (i, j) * u_parent[j]; + + data_on_coarse.d_coeffs[detail_t::wavelet_idx (k, u, i)] = uk[element_t::dg_idx (u, i)] - sum; + } + } + } + + data_on_coarse.vol = data_on_siblings[0].vol * levelmultiindex::NUM_CHILDREN; + data_on_coarse.order = data_on_siblings[0].order; + + TOrderingPolicy::adjust_parent_order (data_on_coarse); + } + + /** + * @brief Volume-scaled detail 2-norm per component. + * + * @param detail A family's detail coefficients and volume. + * @return Per-component detail norm. + */ + [[nodiscard]] static std::array + detail_norm (const detail_t &detail) + { + std::array norm = {}; + const auto &details = detail.d_coeffs; + + for (auto u = 0u; u < U_DIM; ++u) { + auto norm_sq = 0.0; + + for (auto k = 0u; k < levelmultiindex::NUM_CHILDREN; ++k) + for (auto i = 0u; i < DOF; ++i) { + const auto d = details[detail_t::wavelet_idx (k, u, i)]; + norm_sq += d * d; + } + + norm[u] = std::sqrt (norm_sq * TScalingPolicy::detail_norm_scale (detail.vol)); + } + + return norm; + } + + /** + * @brief Forward transform of every complete family in (l_min, l_max]; writes + * their details to d_map, leaves lmi_map unchanged. + * + * @param l_min, l_max Level range, exclusive of l_min. + * @param lmi_map Single-scale leaves. + * @param d_map Output details per family. + * @param mask_coefficients Two-scale mask matrices M[k]. + */ + static void + multiscale_transformation (unsigned int l_min, unsigned int l_max, + levelindex_map &lmi_map, + levelindex_map &d_map, + const std::vector &mask_coefficients) + { + index_set I_set; + detail_t data_on_coarse; + std::array data_on_siblings; + + for (auto l = l_max; l > l_min; --l) { + I_set.reserve (lmi_map.size (l)); + d_map[l - 1].reserve (lmi_map.size (l)); + + for (const auto &[lmi, _] : lmi_map[l]) + I_set.emplace (t8_mra::parent_lmi (lmi)); + + for (const auto &lmi : I_set) { + const auto siblings_lmi = t8_mra::children_lmi (lmi); + + // Incomplete families (siblings on finer levels) carry no detail + // information. + auto family_complete = true; + for (auto k = 0u; k < levelmultiindex::NUM_CHILDREN; ++k) { + const auto *sibling = lmi_map.find (siblings_lmi[k]); + + if (sibling == nullptr) { + family_complete = false; + break; + } + + data_on_siblings[k] = *sibling; + } + + if (!family_complete) + continue; + + two_scale_family (data_on_siblings, data_on_coarse, mask_coefficients); + d_map.insert (lmi, data_on_coarse); + } + + I_set.clear (); + } + } + + /** + * @brief Collapse each complete family down to l_min: replace it by its parent + * in lmi_map (erase children) and write its details to d_map. + * + * @param l_min, l_max Level range, exclusive of l_min. + * @param lmi_map Leaves; collapsed in place. + * @param d_map Output details per family. + * @param mask_coefficients Two-scale mask matrices M[k]. + */ + static void + multiscale_decomposition (unsigned int l_min, unsigned int l_max, levelindex_map &lmi_map, + levelindex_map &d_map, + const std::vector &mask_coefficients) + { + index_set I_set; + detail_t data_on_coarse; + std::array data_on_siblings; + + for (auto l = l_max; l > l_min; --l) { + + for (const auto &[lmi, _] : lmi_map[l]) + I_set.emplace (t8_mra::parent_lmi (lmi)); + + d_map[l - 1].reserve (lmi_map.size (l)); + + for (const auto &lmi : I_set) { + const auto siblings_lmi = t8_mra::children_lmi (lmi); + + // On an adaptive grid a family may be incomplete: some siblings stayed + // refined on finer levels. Such families cannot be two-scale transformed. + auto family_complete = true; + for (auto k = 0u; k < levelmultiindex::NUM_CHILDREN; ++k) { + const auto *sibling = lmi_map.find (siblings_lmi[k]); + + if (sibling == nullptr) { + family_complete = false; + break; + } + + data_on_siblings[k] = *sibling; + } + + if (!family_complete) + continue; + + two_scale_family (data_on_siblings, data_on_coarse, mask_coefficients); + + // The lmi_map leaf keeps only single-scale data (slice off d_coeffs). + lmi_map.insert (lmi, static_cast (data_on_coarse)); + d_map.insert (lmi, data_on_coarse); + + // Consume only this family's children; members of skipped (incomplete) + // families must stay in the map as leaves. + for (auto k = 0u; k < levelmultiindex::NUM_CHILDREN; ++k) + lmi_map.erase (siblings_lmi[k]); + } + + I_set.clear (); + } + } + + /** + * @brief Reconstruct children from parent and detail coefficients over + * [l_min, l_max); moves the data from d_map back into lmi_map. + * + * u_child[k][i] = d[k][i] + Σ_j M[k](i,j) * u_parent[j] + * + * @param l_min, l_max Level range, exclusive of l_max. + * @param lmi_map Parents in, children out. + * @param d_map Details in; consumed. + * @param mask_coefficients Two-scale mask matrices M[k]. + */ + static void + inverse_multiscale_transformation (unsigned int l_min, unsigned int l_max, + levelindex_map &lmi_map, + levelindex_map &d_map, + const std::vector &mask_coefficients) + { + element_t new_data; + const double inv_scaling_factor = TScalingPolicy::inverse_scaling_factor (); + + for (auto l = l_min; l < l_max; ++l) { + lmi_map[l + 1].reserve (d_map[l].size ()); + + for (const auto &[lmi, d] : d_map[l]) { + const auto children_lmi = t8_mra::children_lmi (lmi); + const auto lmi_data = lmi_map.get (lmi); + const auto &details = d.d_coeffs; + const auto &u_parent = lmi_data.u_coeffs; + + // Inverse MST: Reconstruct children u_child[k][i] = d[k][i] + Σ_j M[k](i,j) * u_parent[j] + for (auto k = 0u; k < levelmultiindex::NUM_CHILDREN; ++k) { + const auto &Mk = mask_coefficients[k]; + for (auto u = 0u; u < U_DIM; ++u) { + for (auto i = 0u; i < DOF; ++i) { + auto sum = 0.0; + + for (auto j = 0u; j < DOF; ++j) + sum += u_parent[element_t::dg_idx (u, j)] * Mk (i, j); + + new_data.u_coeffs[element_t::dg_idx (u, i)] + = details[detail_t::wavelet_idx (k, u, i)] + sum * inv_scaling_factor; + } + } + + new_data.vol = lmi_data.vol / levelmultiindex::NUM_CHILDREN; + TOrderingPolicy::adjust_child_order (new_data, k, lmi_data); + + lmi_map.insert (children_lmi[k], new_data); + } + + lmi_map.erase (lmi); + } + + d_map.erase (l); + } + } +}; + +} // namespace t8_mra + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/core/multiscale.hxx b/src/t8_mra/core/multiscale.hxx new file mode 100644 index 0000000000..48ae2e9003 --- /dev/null +++ b/src/t8_mra/core/multiscale.hxx @@ -0,0 +1,567 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include +#include +#include +#include + +#include "ankerl/unordered_dense.h" + +#include "sc_containers.h" +#include "sc_mpi.h" + +#include "t8.h" +#include "t8_cmesh/t8_cmesh.h" +#include "t8_data/t8_containers.h" +#include "t8_eclass/t8_eclass.h" +#include "t8_element/t8_element.h" +#include "t8_forest/t8_forest_general.h" +#include "t8_forest/t8_forest_iterate.h" +#include "t8_schemes/t8_scheme.h" +#include "t8_schemes/t8_scheme.hxx" + +#include "t8_mra/core/adapt/balance.hxx" +#include "t8_mra/core/adapt/coarsen.hxx" +#include "t8_mra/core/adapt/refine.hxx" +#include "t8_mra/core/forest_backend.hxx" +#include "t8_mra/core/mst.hxx" +#include "t8_mra/criteria/coarsening_criterion.hxx" +#include "t8_mra/criteria/refinement_criterion.hxx" +#include "t8_mra/data/element_data.hxx" +#include "t8_mra/data/levelindex_map.hxx" +#include "t8_mra/data/levelindex_set.hxx" +#include "t8_mra/data/levelmultiindex.hxx" +#include "t8_mra/dg/dg_base.hxx" +#include "t8_mra/num/basis/basis.hxx" +#include "t8_mra/num/cell_geometry.hxx" +#include "t8_mra/num/nodal_to_modal.hxx" + +namespace t8_mra +{ + +/** + * @brief Multiresolution analysis on an adaptive t8code forest. + * + * Composition of three collaborators: + * - grid: the t8code + MPI side (forest, lmi_map, ghost, adapt/partition) + * - transform: the multiscale (two-scale) transform on the maps + * - dg: the per-shape DG numerics (projection, evaluation, geometry) + * + * The adaptation operations (coarsen/refine/balance) live as free functions in + * core/adapt/ and are forwarded here. + * + * @tparam TShape element shape, @tparam U components, @tparam P order + */ +template +class multiscale { + public: + using element_t = element_data; + using detail_t = detail_data; + using levelmultiindex = t8_mra::levelmultiindex; + using index_set = ankerl::unordered_dense::set; + using geometry_t = cell_geometry; + using dg_t = dg; + using MST = mst; + + static constexpr auto Shape = TShape; + static constexpr unsigned int DIM = element_t::DIM; + static constexpr unsigned int U_DIM = U; + static constexpr unsigned int P_DIM = P; + static constexpr unsigned int DOF = element_t::DOF; + static constexpr unsigned int W_DOF = element_t::W_DOF; + + //============================================================================= + // Collaborators and state + //============================================================================= + + /// t8code + MPI side + forest_backend grid; + + /// Two-scale transform (owns the mask coefficients) + MST transform; + + /// Per-shape DG numerics + dg_t discretization; + + /// Scaling factors per component (set by criteria via prepare) + std::array c_scaling; + + /// Detail coefficient storage + levelindex_map d_map; + + /// Significant details + levelindex_set td_set; + + /// Elements marked for refinement + levelindex_set refinement_set; + + /// Elements marked for coarsening + levelindex_set coarsening_set; + + multiscale (int max_level, sc_MPI_Comm comm) + : grid (max_level, comm), d_map (grid.maximum_level), td_set (grid.maximum_level), + refinement_set (grid.maximum_level), coarsening_set (grid.maximum_level) + { + c_scaling.fill (1.0); + grid.bind (this, [this] () { post_adapt (); }); + } + + //============================================================================= + // Accessors (forward to grid) + //============================================================================= + + [[nodiscard]] t8_forest_t + get_forest () + { + return grid.get_forest (); + } + + [[nodiscard]] sc_MPI_Comm + get_comm () const + { + return grid.comm; + } + + [[nodiscard]] t8_mra::forest_data * + get_user_data () + { + return grid.get_user_data (); + } + + [[nodiscard]] levelindex_map * + get_lmi_map () + { + return grid.get_lmi_map (); + } + + [[nodiscard]] unsigned int + maximum_level () const + { + return grid.maximum_level; + } + + template + void + for_each_local_leaf (TFunc &&f) + { + grid.for_each_local_leaf (std::forward (f)); + } + + void + ghost_exchange () + { + grid.ghost_exchange (); + } + + void + repartition () + { + grid.repartition (); + } + + //============================================================================= + // Multiscale transform (forward to transform) + //============================================================================= + + void + multiscale_transformation (unsigned int l_min, unsigned int l_max) + { + transform.multiscale_transformation (l_min, l_max, *get_lmi_map (), d_map); + } + + void + inverse_multiscale_transformation (unsigned int l_min, unsigned int l_max) + { + transform.inverse_multiscale_transformation (l_min, l_max, *get_lmi_map (), d_map); + } + + void + multiscale_decomposition (unsigned int l_min, unsigned int l_max) + { + transform.multiscale_decomposition (l_min, l_max, *get_lmi_map (), d_map); + } + + //============================================================================= + // Thresholding + //============================================================================= + + /** @brief max_u ||d_u|| / c_scaling_u. */ + [[nodiscard]] double + scaled_detail_norm (const levelmultiindex &lmi) + { + auto detail_norm = transform.detail_norm (d_map.get (lmi)); + for (auto u = 0u; u < U_DIM; ++u) + detail_norm[u] /= c_scaling[u]; + + return *std::max_element (detail_norm.begin (), detail_norm.end ()); + } + + /** @brief Level-dependent threshold (Veli eq. 2.44). */ + [[nodiscard]] double + local_threshold_value (const levelmultiindex &lmi, int gamma) + { + const auto vol = d_map.get (lmi).vol; + + const auto level_diff = grid.maximum_level - lmi.level (); + const auto h_lambda = std::sqrt (vol); + const auto h_max_level = std::pow (vol / std::pow (levelmultiindex::NUM_CHILDREN, level_diff), (gamma + 1.0) / 2.0); + + return h_max_level / h_lambda; + } + + /** @brief Per-component domain-integral scaling (eq. 2.39), reduced over ranks. */ + [[nodiscard]] std::array + threshold_scaling_factor () + { + std::array res = {}; + + for_each_local_leaf ([&] (t8_locidx_t, const t8_element_t *, unsigned int local_idx, t8_gloidx_t) { + const auto lmi = t8_mra::get_lmi_from_forest_data (get_user_data (), local_idx); + const auto &data = get_lmi_map ()->get (lmi); + const auto mean = mean_val (data); + + for (auto u = 0u; u < U_DIM; ++u) + res[u] += std::abs (mean[u]) * data.vol; + }); + + std::array global_res = {}; + sc_MPI_Allreduce (res.data (), global_res.data (), U_DIM, sc_MPI_DOUBLE, sc_MPI_SUM, grid.comm); + + for (auto u = 0u; u < U_DIM; ++u) + res[u] = std::max (1.0, global_res[u]); + + return res; + } + + //============================================================================= + // Evaluation + //============================================================================= + + /** @brief Solution value per component at a reference-cell point. */ + [[nodiscard]] std::array + evaluate_reference (const element_t &data, const std::array &x_ref) + { + std::array res = {}; + + for (auto u = 0u; u < U_DIM; ++u) + res[u] = geometry_t::reference_value (std::span (&data.u_coeffs[element_t::dg_idx (u, 0)], DOF), + x_ref, data.vol); + + return res; + } + + /** @brief Cell average per component. */ + [[nodiscard]] std::array + mean_val (const element_t &data) + { + std::array mean = {}; + + for (auto u = 0u; u < U_DIM; ++u) + mean[u] = t8_mra::cell_mean (std::span (&data.u_coeffs[element_t::dg_idx (u, 0)], DOF), + data.vol); + + return mean; + } + + [[nodiscard]] std::array + mean_val (const levelmultiindex &lmi) + { + return mean_val (get_lmi_map ()->get (lmi)); + } + + /** @brief Solution value per component at a physical point of the given leaf. */ + [[nodiscard]] std::array + evaluate (int tree_idx, const t8_element_t *element, const element_t &data, const std::array &x_phys) + { + std::array, T8_ECLASS_MAX_CORNERS> corners; + + grid.element_corner_coords (tree_idx, element, corners); + const auto geom = discretization.geometry (corners, data.vol, data.order); + + return discretization.evaluate (geom, data, x_phys); + } + + /** @brief Solution gradient per component at a physical point of the given leaf. */ + [[nodiscard]] std::array, U_DIM> + evaluate_gradient (int tree_idx, const t8_element_t *element, const element_t &data, + const std::array &x_phys) + { + std::array, T8_ECLASS_MAX_CORNERS> corners; + + grid.element_corner_coords (tree_idx, element, corners); + const auto geom = discretization.geometry (corners, data.vol, data.order); + + return discretization.evaluate_gradient (geom, data, x_phys); + } + + /// Point-location query for t8_forest_search, filled with the owning leaf value. + struct point_query + { + std::array point; + double tolerance; + int found; + std::array value; + }; + + static int + search_descend_fn (t8_forest_t /*unused*/, const t8_locidx_t /*unused*/, const t8_element_t * /*unused*/, + const int /*unused*/, const t8_element_array_t * /*unused*/, const t8_locidx_t /*unused*/) + { + return 1; + } + + static void + search_point_fn (t8_forest_t forest, const t8_locidx_t ltreeid, const t8_element_t *element, const int is_leaf, + const t8_element_array_t * /*unused*/, const t8_locidx_t /*unused*/, sc_array_t *queries, + sc_array_t *query_indices, int *query_matches, const size_t num_active_queries) + { + auto *user_data = reinterpret_cast *> (t8_forest_get_user_data (forest)); + auto *mra = static_cast (user_data->mra_instance); + const auto *scheme = t8_forest_get_scheme (forest); + + for (auto i = 0u; i < num_active_queries; ++i) { + const size_t query_idx = *static_cast (sc_array_index (query_indices, i)); + auto *query = static_cast (sc_array_index (queries, query_idx)); + + auto inside = 0; + t8_forest_element_points_inside (forest, ltreeid, element, query->point.data (), 1, &inside, query->tolerance); + query_matches[i] = inside; + + if ((inside != 0) && (is_leaf != 0) && !query->found) { + const auto gtree = t8_forest_global_tree_id (forest, ltreeid); + const auto lmi = levelmultiindex (gtree, element, scheme); + + std::array x; + + for (auto d = 0u; d < DIM; ++d) + x[d] = query->point[d]; + + query->value = mra->evaluate (ltreeid, element, mra->get_lmi_map ()->get (lmi), x); + query->found = 1; + } + } + } + + /** @brief Solution value at a physical point; nullopt if no local leaf owns it. */ + [[nodiscard]] std::optional> + evaluate_point (const std::array &x, double tolerance = 1e-8) + { + point_query query = {}; + for (auto d = 0u; d < DIM; ++d) + query.point[d] = x[d]; + query.tolerance = tolerance; + + sc_array_t *queries = sc_array_new_count (sizeof (point_query), 1); + *static_cast (sc_array_index (queries, 0)) = query; + + t8_forest_search (grid.get_forest (), search_descend_fn, search_point_fn, queries); + + const point_query result = *static_cast (sc_array_index (queries, 0)); + sc_array_destroy (queries); + + if (result.found) + return result.value; + + return std::nullopt; + } + + //============================================================================= + // Projection / initialization + //============================================================================= + + /** @brief Project func onto a single leaf. */ + template + element_t + project_leaf (int tree_idx, const t8_element_t *element, TFunc &&func) + { + element_t data; + data.vol = grid.element_volume (tree_idx, element); + + const auto *scheme = t8_forest_get_scheme (grid.get_forest ()); + data.order = levelmultiindex::point_order_at_level (element, scheme); + + std::array, T8_ECLASS_MAX_CORNERS> corners; + + grid.element_corner_coords (tree_idx, element, corners); + const auto geom = discretization.geometry (corners, data.vol, data.order); + discretization.project (data.u_coeffs, geom, func); + + return data; + } + + /** @brief Project func onto a uniform forest of the given level. */ + template + void + initialize_data (t8_cmesh_t mesh, const t8_scheme *scheme, int level, TFunc &&func) + { + grid.forest = t8_forest_new_uniform (mesh, scheme, level, 0, grid.comm); + grid.build ([&] (int tree_idx, const t8_element_t *element) { return project_leaf (tree_idx, element, func); }); + } + + /** @brief Load per-cell nodal DG values onto an existing forest as modal coeffs. */ + template + void + initialize_data_nodal (t8_forest_t forest, const std::array, DOF> &nodes, + CellNodalValues &&cell_nodal_values) + { + const nodal_to_modal to_modal (nodes); + + t8_forest_ref (forest); + grid.forest = forest; + + grid.build ([&] (int tree_idx, const t8_element_t *element) { + element_t data; + data.vol = grid.element_volume (tree_idx, element); + const auto *scheme = t8_forest_get_scheme (grid.get_forest ()); + data.order = levelmultiindex::point_order_at_level (element, scheme); + const auto nodal = cell_nodal_values (tree_idx, element); + to_modal (std::span (nodal.data (), nodal.size ()), data.u_coeffs); + + return data; + }); + } + + /** @brief Reconstruct per-cell nodal DG values from the current forest. */ + template + void + export_data_nodal (const std::array, DOF> &nodes, + WriteCellNodalValues &&write_cell_nodal_values) + { + const modal_to_nodal to_nodal (nodes); + const auto *scheme = t8_forest_get_scheme (grid.get_forest ()); + auto *lmi_map = get_lmi_map (); + + for_each_local_leaf ( + [&] (t8_locidx_t tree_idx, const t8_element_t *element, unsigned int, t8_gloidx_t global_tree) { + const levelmultiindex lmi (global_tree, element, scheme); + const auto *data = lmi_map->find (lmi); + const auto nodal = to_nodal (std::span (data->u_coeffs.data (), data->u_coeffs.size ())); + write_cell_nodal_values (tree_idx, element, std::span (nodal.data (), nodal.size ())); + }); + } + + //============================================================================= + // Adaptation (forward to adapt::) + //============================================================================= + + template + requires coarsening_criterion + void + coarsen (int min_level, int max_level, TCriterion criterion = {}) + { + adapt::coarsen (*this, min_level, max_level, criterion); + } + + template + requires refinement_criterion + void + refine (int min_level, int max_level, TCriterion criterion = {}) + { + adapt::refine (*this, min_level, max_level, criterion); + } + + void + balance () + { + adapt::balance (*this); + } + + template + requires coarsening_criterion + void + initialize_data_adaptive (t8_cmesh_t mesh, const t8_scheme *scheme, int max_level, TFunc &&func, + TCriterion criterion = {}) + { + adapt::initialize_data_adaptive (*this, mesh, scheme, max_level, func, criterion); + } + + //============================================================================= + // t8code adaptation callbacks + //============================================================================= + + int + coarsening_callback (t8_forest_t /*unused*/, t8_forest_t forest_from, t8_locidx_t which_tree, t8_eclass_t /*unused*/, + t8_locidx_t /*unused*/, const t8_scheme_c *scheme, int is_family, int /*unused*/, + std::span elements) + { + if (is_family == 0) + return 0; + + const auto gtreeid = t8_forest_global_tree_id (forest_from, which_tree); + const auto lmi = levelmultiindex (gtreeid, elements[0], scheme); + + return coarsening_set.contains (lmi) ? -1 : 0; + } + + int + refinement_callback (t8_forest_t, t8_forest_t forest_from, t8_locidx_t which_tree, t8_eclass_t, t8_locidx_t, + const t8_scheme_c *scheme, int, int, std::span elements) + { + const auto gtreeid = t8_forest_global_tree_id (forest_from, which_tree); + const auto lmi = levelmultiindex (gtreeid, elements[0], scheme); + + return refinement_set.contains (lmi) ? 1 : 0; + } + + static int + static_coarsening_callback (t8_forest_t forest, t8_forest_t forest_from, t8_locidx_t which_tree, + t8_eclass_t tree_class, t8_locidx_t local_ele_idx, const t8_scheme_c *scheme, + int is_family, int num_elements, t8_element_t *elements[]) + { + auto *user_data = reinterpret_cast *> (t8_forest_get_user_data (forest_from)); + + return static_cast (user_data->mra_instance) + ->coarsening_callback (forest, forest_from, which_tree, tree_class, local_ele_idx, scheme, is_family, + num_elements, { elements, static_cast (num_elements) }); + } + + static int + static_refinement_callback (t8_forest_t forest, t8_forest_t forest_from, t8_locidx_t which_tree, + t8_eclass_t tree_class, t8_locidx_t local_ele_idx, const t8_scheme_c *scheme, + int is_family, int num_elements, t8_element_t *elements[]) + { + auto *user_data = reinterpret_cast *> (t8_forest_get_user_data (forest_from)); + + return static_cast (user_data->mra_instance) + ->refinement_callback (forest, forest_from, which_tree, tree_class, local_ele_idx, scheme, is_family, + num_elements, { elements, static_cast (num_elements) }); + } + + //============================================================================= + // Post-adaptation hook + cleanup + //============================================================================= + + /** @brief Refresh the per-leaf vertex order (triangle Bey type); no-op values for cartesian. */ + void + post_adapt () + { + if (grid.get_forest () == nullptr) + return; + + auto *user_data = grid.get_user_data (); + const auto *scheme = t8_forest_get_scheme (grid.get_forest ()); + + grid.for_each_local_leaf ([&] (t8_locidx_t, const t8_element_t *elem, unsigned int local_idx, t8_gloidx_t) { + const auto lmi = t8_mra::get_lmi_from_forest_data (user_data, local_idx); + if (auto *data = user_data->lmi_map->find (lmi)) + data->order = levelmultiindex::point_order_at_level (elem, scheme); + }); + } + + void + cleanup () + { + d_map.erase_all (); + td_set.erase_all (); + refinement_set.erase_all (); + coarsening_set.erase_all (); + grid.cleanup (); + } +}; + +} // namespace t8_mra + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/core/shape/cartesian.hxx b/src/t8_mra/core/shape/cartesian.hxx new file mode 100644 index 0000000000..6acb23b955 --- /dev/null +++ b/src/t8_mra/core/shape/cartesian.hxx @@ -0,0 +1,102 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include "t8_eclass/t8_eclass.h" + +#include "t8_mra/core/shape/mst_policy.hxx" +#include "t8_mra/core/shape_traits.hxx" + +namespace t8_mra +{ + +template <> +struct shape_traits +{ + static constexpr unsigned short DIM = 1; + static constexpr unsigned short NUM_CHILDREN = 2; + static constexpr int NUM_VERTICES = 2; + static constexpr int VTK_CELL_TYPE = 68; // VTK_LAGRANGE_CURVE + + [[nodiscard]] static constexpr unsigned short + dof (unsigned short P) + { + return P; + } +}; + +template <> +struct shape_traits +{ + static constexpr unsigned short DIM = 2; + static constexpr unsigned short NUM_CHILDREN = 4; + static constexpr int NUM_VERTICES = 4; + static constexpr int VTK_CELL_TYPE = 70; // VTK_LAGRANGE_QUADRILATERAL + + [[nodiscard]] static constexpr unsigned short + dof (unsigned short P) + { + return P * P; + } +}; + +template <> +struct shape_traits +{ + static constexpr unsigned short DIM = 3; + static constexpr unsigned short NUM_CHILDREN = 8; + static constexpr int NUM_VERTICES = 8; + static constexpr int VTK_CELL_TYPE = 72; // VTK_LAGRANGE_HEXAHEDRON + + [[nodiscard]] static constexpr unsigned short + dof (unsigned short P) + { + return P * P * P; + } +}; + +/// Cartesian shapes carry no vertex-order information. +template + requires is_cartesian +struct ordering_policy +{ + template + static void + adjust_parent_order (TData & /*unused*/) + { + } + + template + static void + adjust_child_order (TData & /*unused*/, int /*unused*/, const TData & /*unused*/) + { + } +}; + +/// Cartesian MST scaling: L2-orthonormal reference basis +template + requires is_cartesian +struct mst_scaling_policy +{ + [[nodiscard]] static constexpr double + forward_scaling_factor (unsigned int num_children) + { + return 1.0 / static_cast (num_children); + } + + [[nodiscard]] static constexpr double + inverse_scaling_factor () + { + return 1.0; + } + + [[nodiscard]] static constexpr double + detail_norm_scale (double /*unused*/) + { + return 1.0; + } +}; + +} // namespace t8_mra + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/core/shape/mst_policy.hxx b/src/t8_mra/core/shape/mst_policy.hxx new file mode 100644 index 0000000000..440d25f311 --- /dev/null +++ b/src/t8_mra/core/shape/mst_policy.hxx @@ -0,0 +1,22 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include + +namespace t8_mra +{ + +/// Per-shape vertex-order handling under refinement. Specialized per shape in +/// core/shape/. +template +struct ordering_policy; + +/// Per-shape MST normalization and detail-norm scaling. Specialized per shape in +/// core/shape/. +template +struct mst_scaling_policy; + +} // namespace t8_mra + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/core/shape/triangle.hxx b/src/t8_mra/core/shape/triangle.hxx new file mode 100644 index 0000000000..e3ed948de6 --- /dev/null +++ b/src/t8_mra/core/shape/triangle.hxx @@ -0,0 +1,74 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include "t8_eclass/t8_eclass.h" + +#include "t8_mra/core/shape/mst_policy.hxx" +#include "t8_mra/core/shape_traits.hxx" +#include "t8_mra/data/triangle_order.hxx" + +namespace t8_mra +{ + +template <> +struct shape_traits +{ + static constexpr unsigned short DIM = 2; + static constexpr unsigned short NUM_CHILDREN = 4; + static constexpr int NUM_VERTICES = 3; + static constexpr int VTK_CELL_TYPE = 69; // VTK_LAGRANGE_TRIANGLE + + [[nodiscard]] static constexpr unsigned short + dof (unsigned short P) + { + return binom (DIM + P - 1, DIM); + } +}; + +/// Triangle vertex order follows the Bey refinement type across levels. +template <> +struct ordering_policy +{ + template + static void + adjust_parent_order (TData &data) + { + triangle_order::get_parent_order (data.order); + } + + template + static void + adjust_child_order (TData &child_data, int child_id, const TData &parent_data) + { + child_data.order = parent_data.order; + triangle_order::get_point_order (child_data.order, child_id); + } +}; + +/// Triangle MST scaling: no child averaging in the forward transform +template <> +struct mst_scaling_policy +{ + [[nodiscard]] static constexpr double + forward_scaling_factor (unsigned int /*unused*/) + { + return 1.0; + } + + [[nodiscard]] static constexpr double + inverse_scaling_factor () + { + return 1.0; + } + + [[nodiscard]] static constexpr double + detail_norm_scale (double vol) + { + return 1.0 / vol; + } +}; + +} // namespace t8_mra + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/core/shape_traits.hxx b/src/t8_mra/core/shape_traits.hxx new file mode 100644 index 0000000000..9dc1c83ff2 --- /dev/null +++ b/src/t8_mra/core/shape_traits.hxx @@ -0,0 +1,62 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include + +#include + +namespace t8_mra +{ + +// ============================================================================ +// Adding a new element shape +// ============================================================================ +// Every per-shape specialization lives in one file per shape: shape_traits + +// mst policies in core/shape/, lmi layout in data/shape/, basis + cell_geometry +// in num/shape/, DG numerics in dg/. This header holds only the primary +// template and pulls the specializations in at the bottom. + +template +concept is_cartesian = (TShape == T8_ECLASS_LINE || TShape == T8_ECLASS_QUAD || TShape == T8_ECLASS_HEX); + +/// Binomial coefficient (compile-time), for simplex DOF counts. +[[nodiscard]] constexpr size_t +binom (size_t n, size_t k) noexcept +{ + if (k > n) + return 0; + k = std::min (n - k, k); + + auto result = 1u; + for (size_t i = 0; i < k; ++i) + result = result * (n - i) / (i + 1); + + return result; +} + +/// Per-shape compile-time facts. Primary template left undefined-ish (DIM 0) +/// so an unsupported shape fails loudly where the values are used. +template +struct shape_traits +{ + static constexpr unsigned short DIM = 0; + static constexpr unsigned short NUM_CHILDREN = 0; + static constexpr int NUM_VERTICES = 0; + static constexpr int VTK_CELL_TYPE = 0; + + [[nodiscard]] static constexpr unsigned short + dof (unsigned short /*unused*/) + { + return 0; + } +}; + +} // namespace t8_mra + +// Per-shape specializations (defined after the primary template). +#include "t8_mra/core/shape/cartesian.hxx" +#include "t8_mra/core/shape/triangle.hxx" + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/criteria/coarsening_criterion.hxx b/src/t8_mra/criteria/coarsening_criterion.hxx new file mode 100644 index 0000000000..09f2fb9705 --- /dev/null +++ b/src/t8_mra/criteria/coarsening_criterion.hxx @@ -0,0 +1,72 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include + +namespace t8_mra +{ + +/** + * @brief Requirements for a coarsening criterion + * + * A coarsening criterion decides per leaf family (identified by the parent + * lmi, whose detail data is available in mra.d_map) whether the family's + * detail information is essential: + * + * - significant(mra, lmi) == true: the family keeps its details and + * stays refined. + * - significant(mra, lmi) == false: the details are discarded and the + * family is coarsened into its parent. + * + * Optionally a criterion can provide prepare(mra), which is called once at + * the beginning of every coarsen() call (e.g. to compute global + * normalization factors). + */ +template +concept coarsening_criterion + = requires (TCriterion criterion, TMultiscale &mra, const typename TMultiscale::levelmultiindex &lmi) { + { criterion.significant (mra, lmi) } -> std::convertible_to; + }; + +/** + * @brief Detect optional prepare() hook of a criterion + */ +template +concept criterion_has_prepare = requires (TCriterion criterion, TMultiscale &mra) { criterion.prepare (mra); }; + +/** + * @brief Default coarsening criterion: hard thresholding + * + * Uses the level-dependent threshold of Veli eq. (2.44) on the scaled + * detail norms: + * + * significant: max_u ||d_u|| / c_scaling_u > c_thresh * eps(lmi) + * + * prepare() computes the global scaling factors c_scaling (eq. 2.39). + */ +struct hard_thresholding +{ + /// Threshold constant + double c_thresh = 1.0; + /// Expected order of convergence + int gamma = 1; + + template + void + prepare (TMultiscale &mra) + { + mra.c_scaling = mra.threshold_scaling_factor (); + } + + template + [[nodiscard]] bool + significant (TMultiscale &mra, const typename TMultiscale::levelmultiindex &lmi) + { + return mra.scaled_detail_norm (lmi) > c_thresh * mra.local_threshold_value (lmi, gamma); + } +}; + +} // namespace t8_mra + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/criteria/refinement_criterion.hxx b/src/t8_mra/criteria/refinement_criterion.hxx new file mode 100644 index 0000000000..68f0f40fdc --- /dev/null +++ b/src/t8_mra/criteria/refinement_criterion.hxx @@ -0,0 +1,76 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include + +namespace t8_mra +{ + +/** + * @brief Per-family refinement decision + * + * The two flags are independent: + * - grade_neighbours: grade the surrounding grid so the family's face + * neighbours reach its leaf level. + * - refine_children: refine the family's children one further level. + */ +struct refinement_flags +{ + bool grade_neighbours; + bool refine_children; +}; + +/** + * @brief Requirements for a refinement criterion + * + * operator()(mra, lmi) returns the refinement_flags for a leaf family + * (identified by the parent lmi, whose detail is in mra.d_map). Optionally a + * criterion provides prepare(mra), called once at the start of every refine(). + */ +template +concept refinement_criterion + = requires (TCriterion criterion, TMultiscale &mra, const typename TMultiscale::levelmultiindex &lmi) { + { criterion (mra, lmi) } -> std::convertible_to; + }; + +/** + * @brief Example refinement criterion: Harten's prediction + * + * On the scaled detail norm N = max_u ||d_u|| / c_scaling_u and the level + * threshold eps(lmi): + * grade_neighbours: N > c_thresh * eps + * refine_children: N > 2^(P+1) * c_thresh * eps + * + * prepare() computes the global scaling factors c_scaling. + */ +struct harten_prediction +{ + /// Threshold constant + double c_thresh = 1.0; + /// Expected order of convergence (enters the level-dependent threshold) + int gamma = 1; + + template + void + prepare (TMultiscale &mra) + { + mra.c_scaling = mra.threshold_scaling_factor (); + } + + template + [[nodiscard]] refinement_flags + operator() (TMultiscale &mra, const typename TMultiscale::levelmultiindex &lmi) + { + const auto norm = mra.scaled_detail_norm (lmi); + const auto threshold = c_thresh * mra.local_threshold_value (lmi, gamma); + const auto steep_factor = std::pow (2.0, static_cast (TMultiscale::P_DIM) + 1); + + return { norm > threshold, norm > steep_factor * threshold }; + } +}; + +} // namespace t8_mra + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/data/element_data.hxx b/src/t8_mra/data/element_data.hxx new file mode 100644 index 0000000000..36f631babc --- /dev/null +++ b/src/t8_mra/data/element_data.hxx @@ -0,0 +1,94 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include + +#include "sc_containers.h" + +#include "t8_eclass/t8_eclass.h" + +#include "t8_mra/core/shape_traits.hxx" +#include "t8_mra/data/levelindex_map.hxx" +#include "t8_mra/data/levelmultiindex.hxx" + +namespace t8_mra +{ + +/// Per-cell DG data. Shape facts come from shape_traits. +template +struct element_data +{ + static constexpr t8_eclass Shape = TShape; + static constexpr unsigned short DIM = shape_traits::DIM; + static constexpr unsigned short NUM_CHILDREN = shape_traits::NUM_CHILDREN; + static constexpr unsigned short U_DIM = U; + + static constexpr unsigned short P_DIM = P; + static constexpr unsigned short DOF = shape_traits::dof (P); + static constexpr unsigned short W_DOF = DOF * NUM_CHILDREN; + + // Fixed-size storage: keeps the struct trivially copyable, so element + // data can be shipped between ranks as raw bytes (repartitioning, ghost + // exchange). + std::array u_coeffs = {}; // Single-scale coefficients + double vol = 0.0; + + std::array order = {}; // Point order + + [[nodiscard]] static size_t + dg_idx (size_t u, size_t p) noexcept + { + return u * DOF + p; + } +}; + +/// Leaf data plus detail (wavelet) coefficients. +template +struct detail_data: element_data +{ + using base = element_data; + + std::array d_coeffs = {}; + + [[nodiscard]] static size_t + wavelet_idx (size_t k, size_t u, size_t p) noexcept + { + return k * base::U_DIM * base::DOF + u * base::DOF + p; + } +}; + +/// Forest user data: the leaf-index array (lmi per leaf) and the lmi_map. +template +struct forest_data +{ + using lmi_type = levelmultiindex; + + sc_array_t *lmi_idx; + t8_mra::levelindex_map *lmi_map; + + void *mra_instance; // Pointer to multiscale object for callbacks +}; + +/// The lmi of local leaf idx, read from the forest user data. +template +[[nodiscard]] t8_mra::levelmultiindex +get_lmi_from_forest_data (const t8_mra::forest_data *forest_data, size_t idx) +{ + return *reinterpret_cast *> ( + t8_sc_array_index_locidx (forest_data->lmi_idx, idx)); +} + +/// Store the lmi of local leaf idx into the forest user data. +template +void +set_lmi_forest_data (t8_mra::forest_data *forest_data, size_t idx, + const t8_mra::levelmultiindex &lmi) +{ + *reinterpret_cast *> (t8_sc_array_index_locidx (forest_data->lmi_idx, idx)) + = lmi; +} + +} // namespace t8_mra + +#endif diff --git a/src/t8_mra/data/levelindex_map.hxx b/src/t8_mra/data/levelindex_map.hxx new file mode 100644 index 0000000000..ce37ee08f3 --- /dev/null +++ b/src/t8_mra/data/levelindex_map.hxx @@ -0,0 +1,371 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include + +#include +#include + +#include "t8_mra/data/levelmultiindex.hxx" + +namespace t8_mra +{ + +/** + * @brief Per-level hash maps holding an adaptive grid's cell data, keyed by lmi. + * + * O(1) find/insert/erase via the dense hash map of unordered_dense + * (https://github.com/martinus/unordered_dense); one map per refinement level. + * + * @tparam TLmi levelmultiindex key type + * @tparam TData per-cell value type + */ +template +class levelindex_map { + public: + using map = ankerl::unordered_dense::map; + + using iterator = typename map::iterator; + using const_iterator = typename map::const_iterator; + + std::vector level_map; + unsigned int max_level; + + levelindex_map () = default; + explicit levelindex_map (unsigned int _max_level); + + levelindex_map (const levelindex_map &other) = default; + levelindex_map (levelindex_map &&other) noexcept = default; + levelindex_map & + operator= (const levelindex_map &other) = default; + levelindex_map & + operator= (levelindex_map &&other) noexcept = default; + + /** @brief Insert data at (level, key). */ + void + insert (unsigned int level, size_t key, const TData &data); + + /** @brief Insert data at the given lmi. */ + void + insert (const TLmi &lmi, const TData &data); + + /** @brief Erase the entry at (level, key). */ + void + erase (unsigned int level, size_t key); + + /** @brief Erase the entry at the given lmi. */ + void + erase (const TLmi &lmi); + + /** @brief Erase all entries on a level. */ + void + erase (unsigned int level); + + /** @brief Erase all entries. */ + void + erase_all (); + + [[nodiscard]] iterator + begin (unsigned int level); + + [[nodiscard]] iterator + end (unsigned int level); + + [[nodiscard]] const_iterator + begin (unsigned int level) const; + + [[nodiscard]] const_iterator + end (unsigned int level) const; + + /** + * @brief Pointer to the data for an lmi, or nullptr if absent. + * + * Combines existence check and access in one lookup; prefer over + * contains + get when the value is used. + */ + [[nodiscard]] TData * + find (const TLmi &lmi); + + [[nodiscard]] const TData * + find (const TLmi &lmi) const; + + /** @brief Whether a cell exists at (level, key). */ + [[nodiscard]] bool + contains (unsigned int level, size_t key) const; + + /** @brief Whether the given lmi exists. */ + [[nodiscard]] bool + contains (const TLmi &lmi) const; + + /** @brief Total number of stored cells. */ + [[nodiscard]] size_t + size () const noexcept; + + /** @brief Number of stored cells on a level. */ + [[nodiscard]] size_t + size (unsigned int level) const noexcept; + + /** @brief All cells of a level. */ + [[nodiscard]] map & + operator[] (unsigned int level); + + /** @brief All cells of a level. */ + [[nodiscard]] const map & + operator[] (unsigned int level) const; + + /** @brief Data at (level, key); aborts if absent. */ + [[nodiscard]] TData & + get (unsigned int level, size_t key); + + /** @brief Data at the given lmi; aborts if absent. */ + [[nodiscard]] TData & + get (const TLmi &lmi); + + [[nodiscard]] const TData & + get (unsigned int level, size_t key) const; + + [[nodiscard]] const TData & + get (const TLmi &lmi) const; + + private: + void + check_level (unsigned int level) const; +}; + +template +levelindex_map::levelindex_map (unsigned int _max_level): max_level (_max_level) +{ + level_map.resize (max_level + 1); +} + +template +void +levelindex_map::insert (unsigned int level, size_t key, const TData &data) +{ + check_level (level); + + TLmi lmi; + lmi.index = key; + level_map[level][lmi] = data; +} + +template +void +levelindex_map::insert (const TLmi &lmi, const TData &data) +{ + insert (lmi.level (), lmi.index, data); +} + +template +void +levelindex_map::erase (unsigned int level, size_t key) +{ + check_level (level); + + TLmi lmi; + lmi.index = key; + level_map[level].erase (lmi); +} + +template +void +levelindex_map::erase (const TLmi &lmi) +{ + erase (lmi.level (), lmi.index); +} + +template +void +levelindex_map::erase (unsigned int level) +{ + check_level (level); + + level_map[level].clear (); +} + +template +void +levelindex_map::erase_all () +{ + for (auto &map : level_map) + map.clear (); +} + +template +typename levelindex_map::iterator +levelindex_map::begin (unsigned int level) +{ + check_level (level); + + return level_map[level].begin (); +} + +template +typename levelindex_map::const_iterator +levelindex_map::begin (unsigned int level) const +{ + check_level (level); + + return level_map[level].begin (); +} + +template +typename levelindex_map::iterator +levelindex_map::end (unsigned int level) +{ + check_level (level); + + return level_map[level].end (); +} + +template +typename levelindex_map::const_iterator +levelindex_map::end (unsigned int level) const +{ + check_level (level); + + return level_map[level].end (); +} + +template +TData * +levelindex_map::find (const TLmi &lmi) +{ + check_level (lmi.level ()); + + auto &m = level_map[lmi.level ()]; + const auto it = m.find (lmi); + + return it == m.end () ? nullptr : &it->second; +} + +template +const TData * +levelindex_map::find (const TLmi &lmi) const +{ + check_level (lmi.level ()); + + const auto &m = level_map[lmi.level ()]; + const auto it = m.find (lmi); + + return it == m.end () ? nullptr : &it->second; +} + +template +bool +levelindex_map::contains (unsigned int level, size_t key) const +{ + check_level (level); + + TLmi lmi; + lmi.index = key; + + return level_map[level].contains (lmi); +} + +template +bool +levelindex_map::contains (const TLmi &lmi) const +{ + return contains (lmi.level (), lmi.index); +} + +template +size_t +levelindex_map::size () const noexcept +{ + auto res = 0u; + + for (const auto &m : level_map) + res += m.size (); + + return res; +} + +template +size_t +levelindex_map::size (unsigned int level) const noexcept +{ + check_level (level); + + return level_map[level].size (); +} + +template +typename levelindex_map::map & +levelindex_map::operator[] (unsigned int level) +{ + check_level (level); + + return level_map[level]; +} + +template +const typename levelindex_map::map & +levelindex_map::operator[] (unsigned int level) const +{ + check_level (level); + + return level_map[level]; +} + +template +TData & +levelindex_map::get (unsigned int level, size_t key) +{ + check_level (level); + + TLmi lmi; + lmi.index = key; + const auto it = level_map[level].find (lmi); + if (it == level_map[level].end ()) + SC_ABORTF ("levelindex_map::get: missing entry (level=%u, index=%zu)", level, key); + + return it->second; +} + +template +TData & +levelindex_map::get (const TLmi &lmi) +{ + return get (lmi.level (), lmi.index); +} + +template +const TData & +levelindex_map::get (unsigned int level, size_t key) const +{ + check_level (level); + + TLmi lmi; + lmi.index = key; + const auto it = level_map[level].find (lmi); + if (it == level_map[level].end ()) + SC_ABORTF ("levelindex_map::get: missing entry (level=%u, index=%zu)", level, key); + + return it->second; +} + +template +const TData & +levelindex_map::get (const TLmi &lmi) const +{ + return get (lmi.level (), lmi.index); +} + +template +void +levelindex_map::check_level (unsigned int level) const +{ +#if T8_ENABLE_DEBUG + if (level >= level_map.size ()) { + throw std::out_of_range ("Level out of range."); + } +#endif +} + +} // namespace t8_mra + +#endif diff --git a/src/t8_mra/data/levelindex_set.hxx b/src/t8_mra/data/levelindex_set.hxx new file mode 100644 index 0000000000..f0e2b00eaf --- /dev/null +++ b/src/t8_mra/data/levelindex_set.hxx @@ -0,0 +1,270 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include + +#include + +#include "t8_mra/data/levelmultiindex.hxx" + +namespace t8_mra +{ + +/** + * @brief Per-level hash sets of an adaptive grid's cells, keyed by lmi. + * + * O(1) contains/insert/erase via the dense hash set of unordered_dense + * (https://github.com/martinus/unordered_dense); one set per refinement level. + * + * @tparam TLmi levelmultiindex key type + */ +template +class levelindex_set { + public: + using set = ankerl::unordered_dense::set; + + using iterator = typename set::iterator; + using const_iterator = typename set::const_iterator; + + std::vector level_set; + unsigned int max_level; + + levelindex_set () = default; + explicit levelindex_set (unsigned int _max_level); + + levelindex_set (const levelindex_set &other) = default; + levelindex_set (levelindex_set &&other) noexcept = default; + levelindex_set & + operator= (const levelindex_set &other) + = default; + levelindex_set & + operator= (levelindex_set &&other) noexcept + = default; + + /** @brief Insert (level, key). */ + void + insert (unsigned int level, size_t key); + + /** @brief Insert a levelmultiindex. */ + void + insert (const TLmi &lmi); + + /** @brief Erase the entry at (level, key). */ + void + erase (unsigned int level, size_t key); + + /** @brief Erase the entry at the given lmi. */ + void + erase (const TLmi &lmi); + + /** @brief Erase all entries on a level. */ + void + erase (unsigned int level); + + /** @brief Erase all entries. */ + void + erase_all (); + + [[nodiscard]] iterator + begin (unsigned int level); + + [[nodiscard]] iterator + end (unsigned int level); + + [[nodiscard]] const_iterator + begin (unsigned int level) const; + + [[nodiscard]] const_iterator + end (unsigned int level) const; + + /** @brief Whether a cell exists at (level, key). */ + [[nodiscard]] bool + contains (unsigned int level, size_t key) const; + + /** @brief Whether the given lmi exists. */ + [[nodiscard]] bool + contains (const TLmi &lmi) const; + + /** @brief Total number of stored cells. */ + [[nodiscard]] size_t + size () const noexcept; + + /** @brief Number of stored cells on a level. */ + [[nodiscard]] size_t + size (unsigned int level) const noexcept; + + /** @brief All cells of a level. */ + [[nodiscard]] set & + operator[] (unsigned int level); + + /** @brief All cells of a level. */ + [[nodiscard]] const set & + operator[] (unsigned int level) const; + + private: + void + check_level (unsigned int level) const; +}; + +template +inline levelindex_set::levelindex_set (unsigned int _max_level): max_level (_max_level) +{ + level_set.resize (max_level + 1); +} + +template +inline void +levelindex_set::insert (unsigned int level, size_t key) +{ + check_level (level); + + level_set[level].insert (key); +} + +template +void +levelindex_set::insert (const TLmi &lmi) +{ + insert (lmi.level (), lmi.index); +} + +template +inline void +levelindex_set::erase (unsigned int level, size_t key) +{ + check_level (level); + + level_set[level].erase (key); +} + +template +void +levelindex_set::erase (const TLmi &lmi) +{ + erase (lmi.level (), lmi.index); +} + +template +inline void +levelindex_set::erase (unsigned int level) +{ + check_level (level); + + level_set[level].clear (); +} + +template +inline void +levelindex_set::erase_all () +{ + for (auto &set : level_set) + set.clear (); +} + +template +inline typename levelindex_set::iterator +levelindex_set::begin (unsigned int level) +{ + check_level (level); + + return level_set[level].begin (); +} + +template +inline typename levelindex_set::const_iterator +levelindex_set::begin (unsigned int level) const +{ + check_level (level); + + return level_set[level].begin (); +} + +template +inline typename levelindex_set::iterator +levelindex_set::end (unsigned int level) +{ + check_level (level); + + return level_set[level].end (); +} + +template +typename levelindex_set::const_iterator +levelindex_set::end (unsigned int level) const +{ + check_level (level); + + return level_set[level].end (); +} + +template +inline bool +levelindex_set::contains (unsigned int level, size_t key) const +{ + check_level (level); + + return level_set[level].contains (key); +} + +template +bool +levelindex_set::contains (const TLmi &lmi) const +{ + return contains (lmi.level (), lmi.index); +} + +template +inline size_t +levelindex_set::size () const noexcept +{ + auto res = 0u; + + for (const auto &m : level_set) + res += m.size (); + + return res; +} + +template +inline size_t +levelindex_set::size (unsigned int level) const noexcept +{ + check_level (level); + + return level_set[level].size (); +} + +template +inline typename levelindex_set::set & +levelindex_set::operator[] (unsigned int level) +{ + check_level (level); + + return level_set[level]; +} + +template +inline const typename levelindex_set::set & +levelindex_set::operator[] (unsigned int level) const +{ + check_level (level); + + return level_set[level]; +} + +template +inline void +levelindex_set::check_level (unsigned int level) const +{ +#if T8_ENABLE_DEBUG + if (level >= level_set.size ()) { + throw std::out_of_range ("Level out of range."); + } +#endif +} + +} // namespace t8_mra + +#endif diff --git a/src/t8_mra/data/levelmultiindex.hxx b/src/t8_mra/data/levelmultiindex.hxx new file mode 100644 index 0000000000..e4225075c1 --- /dev/null +++ b/src/t8_mra/data/levelmultiindex.hxx @@ -0,0 +1,180 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include + +#include +#include +#include + +namespace t8_mra +{ + +// ============================================================================ +// Adding a new element shape +// ============================================================================ +// In data/shape/.hxx: +// 1. Specialize lmi_properties with the bit widths (PATH_BITS = +// log2(NUM_CHILDREN), LEVEL_BITS, BASECELL_BITS; their sum must be <= 64) +// and NUM_CHILDREN. That alone gives a working lmi for any shape whose +// children carry no vertex-ordering information (all cartesian shapes). +// 2. Only if the shape's vertex order changes under refinement (like the +// simplex Bey refinement): specialize the element constructor and +// point_order_at_level (see data/shape/triangle.hxx). + +/// Bit layout (path | level | basecell) and child count of an lmi, per shape. +template +struct lmi_properties +{ + static constexpr int PATH_BITS = 0; + static constexpr int LEVEL_BITS = 0; + static constexpr int BASECELL_BITS = 0; + static constexpr int NUM_CHILDREN = 0; +}; + +/** + * @brief Cell identifier: (base tree, level, path) packed into one size_t. + * + * Describes any cell of an adaptive grid by its refinement level and the + * child-id path from its base tree, independently of which rank holds it. + * The bit layout and child count come from lmi_properties; the + * index arithmetic below is shape-independent. Only the construction from a + * forest element and the vertex order can be shape-specific (see TRIANGLE). + * See http://www.esaim-proc.org/10.1051/proc/201134003 + */ +template +struct levelmultiindex +{ + static constexpr auto ECLASS = TShape; + static constexpr int PATH_BITS = lmi_properties::PATH_BITS; + static constexpr int LEVEL_BITS = lmi_properties::LEVEL_BITS; + static constexpr int BASECELL_BITS = lmi_properties::BASECELL_BITS; + static constexpr int NUM_CHILDREN = lmi_properties::NUM_CHILDREN; + + size_t index; + + levelmultiindex () = default; + + levelmultiindex (size_t basecell) noexcept: index (basecell) + { + } + + /// Construct from a forest element by walking its ancestor child-ids down to + /// its level. Generic (cartesian) form; TRIANGLE specializes this to also + /// track the vertex order. + levelmultiindex (size_t basecell, const t8_element_t *elem, const t8_scheme *scheme) noexcept: index (basecell) + { + const auto level = scheme->element_get_level (ECLASS, elem); + for (auto l = 0; l < level; ++l) + *this = jth_child (*this, scheme->element_get_ancestor_id (ECLASS, elem, l + 1)); + } + + [[nodiscard]] bool + operator== (const levelmultiindex &other) const noexcept + { + return index == other.index; + } + + [[nodiscard]] unsigned int + level () const noexcept + { + return static_cast ((index >> BASECELL_BITS) & ((1ULL << LEVEL_BITS) - 1)); + } + + /// Child j: append j to the path, increment the level. + [[nodiscard]] static levelmultiindex + jth_child (levelmultiindex lmi, size_t j) noexcept + { + const auto basecell = lmi.index & ((1ULL << BASECELL_BITS) - 1); + lmi.index >>= BASECELL_BITS; + const auto level = lmi.index & ((1ULL << LEVEL_BITS) - 1); + lmi.index >>= LEVEL_BITS; + + const auto jth_path = (lmi.index << PATH_BITS) | j; + lmi.index = (jth_path << (LEVEL_BITS + BASECELL_BITS)) | ((level + 1) << BASECELL_BITS) | basecell; + + return lmi; + } + + /// Parent: drop the last path segment, decrement the level. + [[nodiscard]] static levelmultiindex + parent (levelmultiindex lmi) + { +#if T8_ENABLE_DEBUG + if (lmi.level () == 0) + SC_ABORTF ("levelmultiindices on level 0 do not have a parent %zu", lmi.index); +#endif + const auto basecell = lmi.index & ((1ULL << BASECELL_BITS) - 1); + lmi.index >>= BASECELL_BITS; + const auto level = lmi.index & ((1ULL << LEVEL_BITS) - 1); + lmi.index >>= LEVEL_BITS; + lmi.index >>= PATH_BITS; + + lmi.index = (lmi.index << (BASECELL_BITS + LEVEL_BITS)) | ((level - 1) << BASECELL_BITS) | basecell; + + return lmi; + } + + [[nodiscard]] static std::array + children (levelmultiindex lmi) noexcept + { + std::array child_vec; + for (auto j = 0u; j < NUM_CHILDREN; ++j) + child_vec[j] = jth_child (lmi, j); + + return child_vec; + } + + /// Reference vertex order at the element's level. Cartesian elements need no + /// reordering (identity); TRIANGLE specializes. + [[nodiscard]] static std::array + point_order_at_level (const t8_element_t * /*unused*/, const t8_scheme * /*unused*/) noexcept + { + return { 0, 1, 2 }; + } +}; + +/// Concept: an lmi is a levelmultiindex of its own ECLASS. +template +concept lmi_type = std::is_same_v>; + +template +[[nodiscard]] inline TLmi +parent_lmi (TLmi lmi) +{ + return TLmi::parent (lmi); +} + +template +[[nodiscard]] inline std::array +children_lmi (TLmi lmi) +{ + return TLmi::children (lmi); +} + +} // namespace t8_mra + +namespace std +{ + +template +struct hash +{ + using is_transparent = void; + + [[nodiscard]] size_t + operator() (const TLmi &lmi) const noexcept + { + return lmi.index; + } +}; + +} // namespace std + +// Per-shape specializations (defined after the primary template). +#include "t8_mra/data/shape/cartesian.hxx" +#include "t8_mra/data/shape/triangle.hxx" + +#endif diff --git a/src/t8_mra/data/shape/cartesian.hxx b/src/t8_mra/data/shape/cartesian.hxx new file mode 100644 index 0000000000..be0fe76516 --- /dev/null +++ b/src/t8_mra/data/shape/cartesian.hxx @@ -0,0 +1,41 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include "t8_eclass/t8_eclass.h" + +#include "t8_mra/data/levelmultiindex.hxx" + +namespace t8_mra +{ + +template <> +struct lmi_properties +{ + static constexpr int PATH_BITS = 1; + static constexpr int LEVEL_BITS = 6; + static constexpr int BASECELL_BITS = 25; + static constexpr int NUM_CHILDREN = 2; +}; + +template <> +struct lmi_properties +{ + static constexpr int PATH_BITS = 2; + static constexpr int LEVEL_BITS = 5; + static constexpr int BASECELL_BITS = 21; + static constexpr int NUM_CHILDREN = 4; +}; + +template <> +struct lmi_properties +{ + static constexpr int PATH_BITS = 3; + static constexpr int LEVEL_BITS = 5; + static constexpr int BASECELL_BITS = 20; + static constexpr int NUM_CHILDREN = 8; +}; + +} // namespace t8_mra + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/data/shape/triangle.hxx b/src/t8_mra/data/shape/triangle.hxx new file mode 100644 index 0000000000..cffe7d7187 --- /dev/null +++ b/src/t8_mra/data/shape/triangle.hxx @@ -0,0 +1,74 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include + +#include "t8_eclass/t8_eclass.h" +#include "t8_element/t8_element.h" +#include "t8_schemes/t8_default/t8_default_tri/t8_dtri.h" +#include "t8_schemes/t8_default/t8_default_tri/t8_dtri_bits.h" +#include "t8_schemes/t8_default/t8_default_tri/t8_dtri_connectivity.h" +#include "t8_schemes/t8_scheme.hxx" + +#include "t8_mra/data/levelmultiindex.hxx" +#include "t8_mra/data/triangle_order.hxx" + +namespace t8_mra +{ + +template <> +struct lmi_properties +{ + static constexpr int PATH_BITS = 2; + static constexpr int LEVEL_BITS = 5; + static constexpr int BASECELL_BITS = 21; + static constexpr int NUM_CHILDREN = 4; +}; + +// The triangle vertex order changes with the Bey refinement type, so the element +// constructor and point_order_at_level track it down the ancestor chain. + +template <> +inline levelmultiindex::levelmultiindex (size_t basecell, const t8_element_t *elem, + const t8_scheme *scheme) noexcept + : index (basecell) +{ + std::array order = { 0, 1, 2 }; + const auto level = scheme->element_get_level (ECLASS, elem); + t8_dtri_t ancestor; + + for (auto l = 0; l < level; ++l) { + auto tmp = order; + + const auto ancestor_id = scheme->element_get_ancestor_id (ECLASS, elem, l + 1); + t8_dtri_ancestor ((t8_dtri_t *) elem, l, &ancestor); + triangle_order::invert_order (tmp); + const auto child_id = triangle_order::get_reference_children_order (ancestor.type, ancestor_id, tmp); + + *this = jth_child (*this, child_id); + triangle_order::get_point_order (order, t8_dtri_type_cid_to_beyid[ancestor.type][ancestor_id]); + } +} + +template <> +inline std::array +levelmultiindex::point_order_at_level (const t8_element_t *elem, const t8_scheme *scheme) noexcept +{ + std::array res = { 0, 1, 2 }; + const auto level = scheme->element_get_level (ECLASS, elem); + t8_dtri_t ancestor; + + for (auto l = 0; l < level; ++l) { + const auto ancestor_id = scheme->element_get_ancestor_id (ECLASS, elem, l + 1); + t8_dtri_ancestor ((t8_dtri_t *) elem, l, &ancestor); + triangle_order::get_point_order (res, t8_dtri_type_cid_to_beyid[ancestor.type][ancestor_id]); + } + + return res; +} + +} // namespace t8_mra + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/data/triangle_order.hxx b/src/t8_mra/data/triangle_order.hxx new file mode 100644 index 0000000000..9c445b7fe3 --- /dev/null +++ b/src/t8_mra/data/triangle_order.hxx @@ -0,0 +1,114 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include + +#include "t8_eclass/t8_eclass.h" +#include +#include +#include +#include + +namespace t8_mra +{ + +struct triangle_order +{ + static constexpr t8_eclass ECLASS = T8_ECLASS_TRIANGLE; + + using perm = std::array; + + static void + get_point_order (perm &order, int cube_id) + { + const auto idx = permutation_index (order, point_perms); + if (idx != -1) + order = point_lookup[idx][cube_id]; + } + + static void + invert_order (perm &order) + { + const auto idx = permutation_index (order, invert_perms); + if (idx != -1) + order = inverse_lookup[idx]; + } + + static void + get_parent_order (perm &order) + { + order = parent_lookup[row_of (order)]; + } + + static int + get_reference_children_order (int type, int child_id, const perm &order) + { + const auto &table = (type == 1) ? child_lookup_type_1 : child_lookup_type_2; + return table[row_of (order)][child_id]; + } + + static void + get_point_order_at_level (size_t basecell, const t8_element_t *elem, const t8_scheme *scheme, perm &order) + { + order = { 0, 1, 2 }; + const auto elem_level = scheme->element_get_level (ECLASS, elem); + t8_dtri_t ancestor; + + for (auto l = 0u; l < elem_level; ++l) { + const auto ancestor_id = scheme->element_get_ancestor_id (ECLASS, elem, l + 1); + t8_dtri_ancestor ((t8_dtri_t *) elem, l, &ancestor); + get_point_order (order, t8_dtri_type_cid_to_beyid[ancestor.type][ancestor_id]); + } + } + + private: + /// Position of a permutation in the table, or -1 if absent. + template + static int + permutation_index (const perm &order, const std::array &table) + { + const auto pos = std::ranges::find (table, order); + return pos == table.end () ? -1 : static_cast (pos - table.begin ()); + } + + /// Row index for the parent/children tables; unknown orders fall back to the last row. + static int + row_of (const perm &order) + { + const auto idx = permutation_index (order, point_perms); + return idx < 0 ? 5 : idx; + } + + static constexpr std::array point_perms + = { { { 0, 1, 2 }, { 2, 0, 1 }, { 1, 2, 0 }, { 0, 2, 1 }, { 1, 0, 2 }, { 2, 1, 0 } } }; + + static constexpr std::array invert_perms + = { { { 0, 1, 2 }, { 0, 2, 1 }, { 1, 2, 0 }, { 1, 0, 2 }, { 2, 0, 1 }, { 2, 1, 0 } } }; + + static constexpr std::array inverse_lookup + = { { { 0, 1, 2 }, { 0, 2, 1 }, { 2, 0, 1 }, { 1, 0, 2 }, { 1, 2, 0 }, { 2, 1, 0 } } }; + + static constexpr std::array, 6> point_lookup = { { + { { { 0, 1, 2 }, { 2, 0, 1 }, { 1, 2, 0 }, { 0, 2, 1 } } }, + { { { 0, 1, 2 }, { 2, 0, 1 }, { 1, 2, 0 }, { 2, 1, 0 } } }, + { { { 0, 1, 2 }, { 2, 0, 1 }, { 1, 2, 0 }, { 1, 0, 2 } } }, + { { { 0, 2, 1 }, { 1, 0, 2 }, { 2, 1, 0 }, { 2, 0, 1 } } }, + { { { 0, 2, 1 }, { 1, 0, 2 }, { 2, 1, 0 }, { 0, 1, 2 } } }, + { { { 0, 2, 1 }, { 1, 0, 2 }, { 2, 1, 0 }, { 1, 2, 0 } } }, + } }; + + static constexpr std::array, 6> child_lookup_type_1 + = { { { 1, 0, 2, 3 }, { 2, 0, 3, 1 }, { 3, 0, 1, 2 }, { 1, 0, 3, 2 }, { 2, 0, 1, 3 }, { 3, 0, 2, 1 } } }; + + static constexpr std::array, 6> child_lookup_type_2 + = { { { 1, 2, 0, 3 }, { 2, 3, 0, 1 }, { 3, 1, 0, 2 }, { 1, 3, 0, 2 }, { 2, 1, 0, 3 }, { 3, 2, 0, 1 } } }; + + static constexpr std::array parent_lookup + = { { { 1, 0, 2 }, { 0, 2, 1 }, { 2, 1, 0 }, { 0, 1, 2 }, { 1, 2, 0 }, { 2, 0, 1 } } }; +}; + +} // namespace t8_mra + +#endif diff --git a/src/t8_mra/dg/cartesian.hxx b/src/t8_mra/dg/cartesian.hxx new file mode 100644 index 0000000000..a095db4f50 --- /dev/null +++ b/src/t8_mra/dg/cartesian.hxx @@ -0,0 +1,161 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include +#include +#include + +#include "t8_eclass/t8_eclass.h" + +#include "t8_mra/core/shape_traits.hxx" +#include "t8_mra/data/element_data.hxx" +#include "t8_mra/dg/dg_base.hxx" +#include "t8_mra/num/cell_geometry.hxx" +#include "t8_mra/num/dg_basis.hxx" +#include "t8_mra/num/geometry.hxx" + +namespace t8_mra +{ + +/** + * @brief Cartesian DG numerics (LINE, QUAD, HEX): tensor Legendre basis, + * Gauss-Legendre projection, axis-aligned box geometry. t8code-free. + */ +template + requires is_cartesian +class dg { + public: + using element_t = element_data; + using geometry_t = cell_geometry; + + static constexpr unsigned int DIM = element_t::DIM; + static constexpr unsigned int U_DIM = U; + static constexpr unsigned int DOF = element_t::DOF; + + /// Gauss-Legendre points per axis: n points integrate degree 2n-1 exactly. + static constexpr int default_quadrature_rule = P + 1; + + dg_basis basis; + + explicit dg (int num_quad_points_1d = default_quadrature_rule): basis (num_quad_points_1d) + { + } + + /** @brief Cell geometry from the leaf's t8code-order corner coords and volume. */ + [[nodiscard]] geometry_t + geometry (const std::array, T8_ECLASS_MAX_CORNERS> &corners, double volume, + const std::array & /*unused*/ = {}) const + { + // QUAD corners are permuted (t8code swaps 2 and 3) so index 0 is the lower + // and the last the upper corner, as extract_cartesian_vertices expects. + std::array, T8_ECLASS_MAX_CORNERS> ordered = {}; + for (auto corner = 0; corner < shape_traits::NUM_VERTICES; ++corner) { + auto source = corner; + + if constexpr (DIM == 2 && TShape == T8_ECLASS_QUAD) { + constexpr std::array quad_corner_order = { 0, 1, 3, 2 }; + source = quad_corner_order[corner]; + } + ordered[corner] = corners[source]; + } + + std::array lower; + std::array upper; + + extract_cartesian_vertices (ordered, lower, upper); + + return geometry_t::from_box (lower, upper, volume); + } + + /** @brief Project func onto the DG basis by Gauss-Legendre quadrature. */ + template + void + project (std::span coeffs, const geometry_t &geom, Func &&func) + { + const auto num_q = basis.quad.num_points; + std::vector> basis_at_quad (num_q); + std::vector> phys_at_quad (num_q); + std::array x_ref; + + for (auto q = 0u; q < num_q; ++q) { + for (unsigned int d = 0; d < DIM; ++d) + x_ref[d] = basis.quad.points[DIM * q + d]; + + basis_at_quad[q] = basis.basis_value (x_ref); + phys_at_quad[q] = geom.to_physical (x_ref); + } + + for (auto i = 0u; i < DOF; ++i) { + std::array sum = {}; + for (auto q = 0u; q < num_q; ++q) { + const auto f_val = eval_func (func, phys_at_quad[q]); + for (auto u = 0u; u < U_DIM; ++u) + sum[u] += basis.quad.weights[q] * f_val[u] * basis_at_quad[q][i]; + } + + for (auto u = 0u; u < U_DIM; ++u) + coeffs[element_t::dg_idx (u, i)] = sum[u]; + } + } + + /** @brief Solution value per component at a physical point. */ + [[nodiscard]] std::array + evaluate (const geometry_t &geom, const element_t &data, const std::array &x_phys) const + { + const auto x_ref = geom.to_reference (x_phys); + std::array res = {}; + + for (auto u = 0u; u < U_DIM; ++u) + res[u] = geom.value (std::span (&data.u_coeffs[element_t::dg_idx (u, 0)], DOF), x_ref); + + return res; + } + + /** @brief Solution gradient grad[u][d] = d(u_u)/d(x_d) at a physical point. */ + [[nodiscard]] std::array, U_DIM> + evaluate_gradient (const geometry_t &geom, const element_t &data, const std::array &x_phys) const + { + const auto x_ref = geom.to_reference (x_phys); + std::array, U_DIM> grad = {}; + + for (auto u = 0u; u < U_DIM; ++u) + grad[u] = geom.gradient (std::span (&data.u_coeffs[element_t::dg_idx (u, 0)], DOF), x_ref); + + return grad; + } + + private: + /// Evaluate func at a physical point; supports func(x{,y,z}) returning an + /// array or writing into an out pointer. + template + [[nodiscard]] static std::array + eval_func (Func &&func, const std::array &x) + { + std::array f_val; + if constexpr (DIM == 1) { + if constexpr (std::is_invocable_v) + f_val = func (x[0]); + else + func (x[0], f_val.data ()); + } + else if constexpr (DIM == 2) { + if constexpr (std::is_invocable_v) + f_val = func (x[0], x[1]); + else + func (x[0], x[1], f_val.data ()); + } + else { + if constexpr (std::is_invocable_v) + f_val = func (x[0], x[1], x[2]); + else + func (x[0], x[1], x[2], f_val.data ()); + } + return f_val; + } +}; + +} // namespace t8_mra + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/dg/dg_base.hxx b/src/t8_mra/dg/dg_base.hxx new file mode 100644 index 0000000000..31dc941237 --- /dev/null +++ b/src/t8_mra/dg/dg_base.hxx @@ -0,0 +1,22 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include + +namespace t8_mra +{ + +/// Per-shape DG numerics of a leaf: projection, evaluation and cell geometry. +/// t8code-free (operates on corner coordinates + volume). Specialized per shape +/// in dg/. +template +class dg; + +} // namespace t8_mra + +// Per-shape specializations (defined after the primary template). +#include "t8_mra/dg/cartesian.hxx" +#include "t8_mra/dg/triangle.hxx" + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/dg/triangle.hxx b/src/t8_mra/dg/triangle.hxx new file mode 100644 index 0000000000..4e73dd83fb --- /dev/null +++ b/src/t8_mra/dg/triangle.hxx @@ -0,0 +1,113 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include +#include + +#include "t8_eclass/t8_eclass.h" + +#include "t8_mra/data/element_data.hxx" +#include "t8_mra/dg/dg_base.hxx" +#include "t8_mra/num/cell_geometry.hxx" +#include "t8_mra/num/dg_basis.hxx" + +namespace t8_mra +{ + +/** + * @brief Triangle DG numerics: orthonormal Dubiner basis, Dunavant projection, + * affine barycentric geometry. t8code-free (vertex order supplied by the caller). + */ +template +class dg { + public: + static constexpr t8_eclass Shape = T8_ECLASS_TRIANGLE; + using element_t = element_data; + using geometry_t = cell_geometry; + + static constexpr unsigned int DIM = element_t::DIM; + static constexpr unsigned int U_DIM = U; + static constexpr unsigned int DOF = element_t::DOF; + + /// Dunavant rule number == polynomial exactness; 2P covers products of two + /// order-P basis functions with margin for general data. + static constexpr int default_quadrature_rule = 2 * P; + + dg_basis basis; + + explicit dg (int dunavant_rule = default_quadrature_rule): basis (dunavant_rule) + { + } + + /** @brief Cell geometry from native corner coords, volume and reference vertex order. */ + [[nodiscard]] geometry_t + geometry (const std::array, T8_ECLASS_MAX_CORNERS> &corners, double volume, + const std::array &order) const + { + std::array, 3> ordered; + for (int i = 0; i < 3; ++i) + ordered[order[i]] = { corners[i][0], corners[i][1] }; + + return geometry_t::from_triangle (ordered[0], ordered[1], ordered[2], volume); + } + + /** @brief Project func onto the DG basis by Dunavant quadrature. */ + template + void + project (std::span coeffs, const geometry_t &geom, Func &&func) + { + const auto num_q = basis.quad.num_points; + std::vector> basis_at_quad (num_q); + std::vector> f_at_quad (num_q); + + for (auto j = 0u; j < num_q; ++j) { + const std::array ref { basis.quad.points[2 * j], basis.quad.points[2 * j + 1] }; + const auto phys = geom.to_physical (ref); + basis_at_quad[j] = basis.basis_value (geom.basis_coord (ref)); + f_at_quad[j] = func (phys[0], phys[1]); + } + + for (auto i = 0u; i < DOF; ++i) { + std::array sum = {}; + + for (auto j = 0u; j < num_q; ++j) + for (auto u = 0u; u < U_DIM; ++u) + sum[u] += basis.quad.weights[j] * f_at_quad[j][u] * geom.basis_scale * basis_at_quad[j][i]; + + for (auto u = 0u; u < U_DIM; ++u) + coeffs[element_t::dg_idx (u, i)] = sum[u] * geom.volume; + } + } + + /** @brief Solution value per component at a physical point. */ + [[nodiscard]] std::array + evaluate (const geometry_t &geom, const element_t &data, const std::array &x_phys) const + { + const auto x_ref = geom.to_reference (x_phys); + std::array res = {}; + + for (auto u = 0u; u < U_DIM; ++u) + res[u] = geom.value (std::span (&data.u_coeffs[element_t::dg_idx (u, 0)], DOF), x_ref); + + return res; + } + + /** @brief Solution gradient grad[u][d] = d(u_u)/d(x_d) at a physical point. */ + [[nodiscard]] std::array, U_DIM> + evaluate_gradient (const geometry_t &geom, const element_t &data, const std::array &x_phys) const + { + const auto x_ref = geom.to_reference (x_phys); + std::array, U_DIM> grad = {}; + + for (auto u = 0u; u < U_DIM; ++u) + grad[u] = geom.gradient (std::span (&data.u_coeffs[element_t::dg_idx (u, 0)], DOF), x_ref); + + return grad; + } +}; + +} // namespace t8_mra + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/io/dataset.hxx b/src/t8_mra/io/dataset.hxx new file mode 100644 index 0000000000..57b315938a --- /dev/null +++ b/src/t8_mra/io/dataset.hxx @@ -0,0 +1,115 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include +#include +#include +#include +#include + +namespace t8_mra +{ + +/** + * @brief Regular-grid sampler usable as an MRA projection func + * + * Holds U components on a DIM-dimensional structured grid and multilinearly + * interpolates them. The call operators take DIM positional doubles and return + * std::array, matching the func signature initialize_data expects, + * so a dataset drops straight into initialize_data / initialize_data_adaptive. + * Points outside the grid box clamp to the boundary. + * + * Value layout: node-major with axis 0 slowest (axis DIM-1 fastest), the U + * components contiguous per node. + */ +template +class structured_dataset { + public: + structured_dataset () = default; + + structured_dataset (const std::array &num_nodes, const std::array &origin, + const std::array &spacing, std::vector values) + : n (num_nodes), origin (origin), spacing (spacing), values (std::move (values)) + { + std::size_t total = U; + for (auto d = 0u; d < DIM; ++d) { + if (n[d] == 0) + throw std::invalid_argument ("structured_dataset: zero nodes on an axis"); + total *= n[d]; + } + if (this->values.size () != total) + throw std::invalid_argument ("structured_dataset: value count does not match grid size * U"); + + stride[DIM - 1] = 1; + for (auto d = DIM - 1; d-- > 0;) + stride[d] = stride[d + 1] * n[d + 1]; + } + + [[nodiscard]] std::array + operator() (double x) const + requires (DIM == 1) + { + return sample ({ x }); + } + + [[nodiscard]] std::array + operator() (double x, double y) const + requires (DIM == 2) + { + return sample ({ x, y }); + } + + [[nodiscard]] std::array + operator() (double x, double y, double z) const + requires (DIM == 3) + { + return sample ({ x, y, z }); + } + + private: + /// Multilinear interpolation at p, clamped to the grid box. + [[nodiscard]] std::array + sample (const std::array &p) const + { + std::array base; + std::array t; + for (auto d = 0u; d < DIM; ++d) { + if (n[d] == 1) { + base[d] = 0; + t[d] = 0.0; + continue; + } + const auto local = std::clamp ((p[d] - origin[d]) / spacing[d], 0.0, static_cast (n[d] - 1)); + const auto i = static_cast (std::clamp (std::floor (local), 0.0, static_cast (n[d] - 2))); + base[d] = i; + t[d] = local - static_cast (i); + } + + std::array out = {}; + for (unsigned int corner = 0; corner < (1u << DIM); ++corner) { + double w = 1.0; + std::size_t flat = 0; + for (auto d = 0u; d < DIM; ++d) { + const bool upper = corner & (1u << d); + flat += (base[d] + (upper ? 1 : 0)) * stride[d]; + w *= upper ? t[d] : 1.0 - t[d]; + } + const auto *node = &values[flat * U]; + for (auto u = 0u; u < U; ++u) + out[u] += w * node[u]; + } + return out; + } + + std::array n = {}; + std::array stride = {}; + std::array origin = {}; + std::array spacing = {}; + std::vector values; +}; + +} // namespace t8_mra + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/io/vtk.hxx b/src/t8_mra/io/vtk.hxx new file mode 100644 index 0000000000..2169c1dd47 --- /dev/null +++ b/src/t8_mra/io/vtk.hxx @@ -0,0 +1,552 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sc_mpi.h" +#include "t8_eclass/t8_eclass.h" +#include "t8.h" +#include "t8_forest/t8_forest_general.h" +#include "t8_forest/t8_forest_geometrical.h" +#include "t8_mra/core/shape_traits.hxx" + +namespace t8_mra +{ + +/** + * @brief Barycentric index of the node at a given position in VTK's Lagrange + * triangle ordering (port of vtkHigherOrderTriangle::BarycentricIndex) + */ +[[nodiscard]] static std::array +vtk_triangle_barycentric_index (int index, int order) +{ + int max = order; + int min = 0; + + // Scope into the correct inner triangle + while (index != 0 && index >= 3 * order) { + index -= 3 * order; + max -= 2; + ++min; + order -= 3; + } + + if (index == 0) + return { min, min, max }; + if (index == 1) + return { max, min, min }; + if (index == 2) + return { min, max, min }; + + std::array bindex; + index -= 3; + const int dim = index / (order - 1); + const int offset = index - dim * (order - 1); + bindex[dim] = min + 1 + offset; + bindex[(dim + 1) % 3] = min; + bindex[(dim + 2) % 3] = max - 1 - offset; + + return bindex; +} + +/** + * @brief Lagrange node positions for a triangle of given order in reference + * coordinates, in VTK's Lagrange triangle ordering + */ +[[nodiscard]] static std::vector> +get_triangle_lagrange_nodes (int order) +{ + const int num_nodes = (order + 1) * (order + 2) / 2; + std::vector> nodes (num_nodes); + + for (auto idx = 0; idx < num_nodes; ++idx) { + const auto bindex = vtk_triangle_barycentric_index (idx, order); + nodes[idx] = { static_cast (bindex[0]) / order, static_cast (bindex[1]) / order }; + } + + return nodes; +} + +/** + * @brief Get Lagrange node positions for a line of given order in reference coordinates + * + * Follows VTK's Lagrange line ordering: + * - First 2 nodes: endpoints + * - Remaining nodes: interior nodes + */ +[[nodiscard]] static std::vector> +get_line_lagrange_nodes (int order) +{ + std::vector> nodes; + nodes.reserve (order + 1); + + // Add endpoints + nodes.push_back ({ 0.0 }); + nodes.push_back ({ 1.0 }); + + // Add interior nodes + for (auto i = 1; i < order; ++i) { + double xi = static_cast (i) / order; + nodes.push_back ({ xi }); + } + + return nodes; +} + +/** + * @brief Index of the grid node (i, j) in VTK's Lagrange quad ordering + * (port of vtkHigherOrderQuadrilateral::PointIndexFromIJK, uniform order) + */ +[[nodiscard]] static int +vtk_quad_point_index (int i, int j, int order) +{ + const bool ibdy = (i == 0 || i == order); + const bool jbdy = (j == 0 || j == order); + + if (ibdy && jbdy) // vertex + return ((i != 0) ? ((j != 0) ? 2 : 1) : ((j != 0) ? 3 : 0)); + + int offset = 4; + if (ibdy || jbdy) { // edge + if (!ibdy) + return (i - 1) + ((j != 0) ? 2 * (order - 1) : 0) + offset; + return (j - 1) + ((i != 0) ? order - 1 : 3 * (order - 1)) + offset; + } + + offset += 4 * (order - 1); // interior + return offset + (i - 1) + (order - 1) * (j - 1); +} + +/** + * @brief Lagrange node positions for a quad of given order in reference + * coordinates, in VTK's Lagrange quad ordering + */ +[[nodiscard]] static std::vector> +get_quad_lagrange_nodes (int order) +{ + const int num_nodes = (order + 1) * (order + 1); + std::vector> nodes (num_nodes); + + for (auto j = 0; j <= order; ++j) + for (auto i = 0; i <= order; ++i) + nodes[vtk_quad_point_index (i, j, order)] = { static_cast (i) / order, static_cast (j) / order }; + + return nodes; +} + +/** + * @brief Index of the grid node (i, j, k) in VTK's Lagrange hex ordering + * (port of vtkHigherOrderHexahedron::PointIndexFromIJK, uniform order) + */ +[[nodiscard]] static int +vtk_hex_point_index (int i, int j, int k, int order) +{ + const bool ibdy = (i == 0 || i == order); + const bool jbdy = (j == 0 || j == order); + const bool kbdy = (k == 0 || k == order); + const int nbdy = (ibdy ? 1 : 0) + (jbdy ? 1 : 0) + (kbdy ? 1 : 0); + + if (nbdy == 3) // vertex + return ((i != 0) ? ((j != 0) ? 2 : 1) : ((j != 0) ? 3 : 0)) + ((k != 0) ? 4 : 0); + + int offset = 8; + if (nbdy == 2) { // edge + if (!ibdy) + return (i - 1) + ((j != 0) ? 2 * (order - 1) : 0) + ((k != 0) ? 4 * (order - 1) : 0) + offset; + if (!jbdy) + return (j - 1) + ((i != 0) ? order - 1 : 3 * (order - 1)) + ((k != 0) ? 4 * (order - 1) : 0) + offset; + offset += 8 * (order - 1); + return (k - 1) + (order - 1) * ((i != 0) ? ((j != 0) ? 2 : 1) : ((j != 0) ? 3 : 0)) + offset; + } + + offset += 12 * (order - 1); + const int face_size = (order - 1) * (order - 1); + if (nbdy == 1) { // face + if (ibdy) + return (j - 1) + (order - 1) * (k - 1) + ((i != 0) ? face_size : 0) + offset; + offset += 2 * face_size; + if (jbdy) + return (i - 1) + (order - 1) * (k - 1) + ((j != 0) ? face_size : 0) + offset; + offset += 2 * face_size; + return (i - 1) + (order - 1) * (j - 1) + ((k != 0) ? face_size : 0) + offset; + } + + offset += 6 * face_size; // interior + + return offset + (i - 1) + (order - 1) * ((j - 1) + (order - 1) * (k - 1)); +} + +/** + * @brief Lagrange node positions for a hex of given order in reference + * coordinates [0,1]^3, in VTK's Lagrange hex ordering + */ +[[nodiscard]] static std::vector> +get_hex_lagrange_nodes (int order) +{ + const int num_nodes = (order + 1) * (order + 1) * (order + 1); + std::vector> nodes (num_nodes); + + for (auto k = 0; k <= order; ++k) + for (auto j = 0; j <= order; ++j) + for (auto i = 0; i <= order; ++i) + nodes[vtk_hex_point_index (i, j, k, order)] + = { static_cast (i) / order, static_cast (j) / order, static_cast (k) / order }; + + return nodes; +} + +/// Reference Lagrange-node layout for a shape, in VTK ordering. Each entry is +/// a point in [0,1]^DIM. Dispatches to the per-shape layout above. +template +[[nodiscard]] auto +get_lagrange_nodes (int order) +{ + if constexpr (TShape == T8_ECLASS_LINE) + return get_line_lagrange_nodes (order); + else if constexpr (TShape == T8_ECLASS_TRIANGLE) + return get_triangle_lagrange_nodes (order); + else if constexpr (TShape == T8_ECLASS_QUAD) + return get_quad_lagrange_nodes (order); + else // HEX + return get_hex_lagrange_nodes (order); +} + +/// t8code-vertex -> VTK-slot permutation for cartesian shapes (triangle vertex +/// order is data-driven, handled at the call site). +template +constexpr auto +vtk_vertex_permutation () +{ + if constexpr (TShape == T8_ECLASS_LINE) + return std::array { 0, 1 }; + else if constexpr (TShape == T8_ECLASS_QUAD) + return std::array { 0, 1, 3, 2 }; + else // HEX + return std::array { 0, 1, 3, 2, 4, 5, 7, 6 }; +} + +/// Reference coords of each VTK-ordered corner, for the multilinear geometry map. +template +constexpr auto +vtk_corner_coords () +{ + if constexpr (TShape == T8_ECLASS_LINE) + return std::array, 2> { { { 0 }, { 1 } } }; + else if constexpr (TShape == T8_ECLASS_QUAD) + return std::array, 4> { { { 0, 0 }, { 1, 0 }, { 1, 1 }, { 0, 1 } } }; + else // HEX + return std::array, 8> { + { { 0, 0, 0 }, { 1, 0, 0 }, { 1, 1, 0 }, { 0, 1, 0 }, { 0, 0, 1 }, { 1, 0, 1 }, { 1, 1, 1 }, { 0, 1, 1 } } + }; +} + +/// Multilinear map of a reference node to physical coords, over VTK-ordered +/// vertices (tensor of (1-x_d)/x_d weights per corner). +template +[[nodiscard]] std::array +map_cartesian (const std::array::DIM> &x, std::span> vertices) +{ + constexpr int DIM = shape_traits::DIM; + constexpr auto corners = vtk_corner_coords (); + + std::array p = { 0, 0, 0 }; + for (auto v = 0; v < corners.size (); ++v) { + double w = 1.0; + for (auto d = 0; d < DIM; ++d) + w *= corners[v][d] == 1.0 ? x[d] : 1.0 - x[d]; + for (auto d = 0; d < 3; ++d) + p[d] += w * vertices[v][d]; + } + + return p; +} + +/** + * @brief Write VTK file header for Lagrange elements + */ +static void +write_vtk_header (std::ofstream &file, int num_points, int num_cells) +{ + file << "\n"; + // Version >= 2.2 required: for older versions VTK's reader assumes the + // pre-9.0 Lagrange hex numbering and permutes the cell connectivity. + file << "\n"; + file << " \n"; + file << " \n"; +} + +/** + * @brief Write VTK footer + */ +static void +write_vtk_footer (std::ofstream &file) +{ + file << " \n"; + file << " \n"; + file << "\n"; +} + +/** + * @brief Write the .pvtu master referencing the per-rank .vtu pieces + */ +static void +write_vtk_master (const char *prefix, int mpisize, int u_dim) +{ + std::ofstream file (std::string (prefix) + ".pvtu"); + + // Piece sources are relative to the master's directory + const std::string p (prefix); + const auto pos = p.find_last_of ('/'); + const auto base = pos == std::string::npos ? p : p.substr (pos + 1); + + file << "\n"; + file << "\n"; + file << " \n"; + file << " \n"; + file << " \n"; + file << " \n"; + file << " \n"; + file << " \n"; + file << " \n"; + file << " \n"; + file << " \n"; + file << " \n"; + + for (auto u = 0; u < u_dim; ++u) + file << R"( \n"; + file << " \n"; + + for (auto rank = 0; rank < mpisize; ++rank) + file << " \n"; + + file << " \n"; + file << "\n"; +} + +/** + * @brief Write a VTK Lagrange file for any TMultiscale implementation + * + * @tparam TMultiscale The multiscale class type (triangle, quad, line, or hex specialization) + * @param mra The multiscale object + * @param prefix Output file prefix + * @param lagrange_order Polynomial order for Lagrange interpolation (P-1) + */ +template +void +write_forest_lagrange_vtk (TMultiscale &mra, const char *prefix, int lagrange_order) +{ + static constexpr auto TShape = TMultiscale::Shape; + static constexpr int U_DIM = TMultiscale::U_DIM; + + t8_forest_t forest = mra.get_forest (); + auto *lmi_map = mra.get_lmi_map (); + + const auto num_local_elements = t8_forest_get_local_num_leaf_elements (forest); + const auto num_local_trees = t8_forest_get_num_local_trees (forest); + + // Lagrange order P-1 has P=lagrange_order+1 nodes per dim; node count is the + // P-basis DOF count of the shape. + constexpr int vtk_cell_type = shape_traits::VTK_CELL_TYPE; + const int num_nodes_per_elem = shape_traits::dof (lagrange_order + 1); + + const int total_points = num_local_elements * num_nodes_per_elem; + + // One piece per rank plus a .pvtu master; single file on one rank + int mpirank = 0; + int mpisize = 1; + sc_MPI_Comm_rank (t8_forest_get_mpicomm (forest), &mpirank); + sc_MPI_Comm_size (t8_forest_get_mpicomm (forest), &mpisize); + + std::string filename + = (mpisize > 1) ? std::string (prefix) + std::format ("_{:04d}.vtu", mpirank) : std::string (prefix) + ".vtu"; + std::ofstream file (filename); + file << std::scientific << std::setprecision (16); + + // Write header + write_vtk_header (file, total_points, num_local_elements); + + // Write points + file << " \n"; + file << " \n"; + + std::vector> all_points; + all_points.reserve (total_points); + + const auto *scheme = t8_forest_get_scheme (forest); + + for (auto tree_idx = 0; tree_idx < num_local_trees; ++tree_idx) { + const auto num_elem_in_tree = t8_forest_get_tree_num_leaf_elements (forest, tree_idx); + + for (auto elem_in_tree = 0; elem_in_tree < num_elem_in_tree; ++elem_in_tree) { + const auto *element = t8_forest_get_leaf_element_in_tree (forest, tree_idx, elem_in_tree); + + // Get LMI to look up vertex ordering (for triangles) + const auto base_tree = t8_forest_global_tree_id (forest, tree_idx); + const auto lmi = typename TMultiscale::levelmultiindex (base_tree, element, scheme); + + // Vertices in VTK order: triangle order is data-driven (point_order from + // the element data), cartesian shapes use a fixed permutation. + std::array, 8> vertices = {}; + + if constexpr (TShape == T8_ECLASS_TRIANGLE) { + const auto *elem_data = lmi_map->find (lmi); + for (auto v = 0; v < 3; ++v) { + std::array coords; + t8_forest_element_coordinate (forest, tree_idx, element, v, coords.data ()); + const int slot = elem_data ? elem_data->order[v] : v; + vertices[slot] = coords; + } + } + else { + constexpr auto perm = vtk_vertex_permutation (); + for (auto i = 0u; i < perm.size (); ++i) + t8_forest_element_coordinate (forest, tree_idx, element, perm[i], vertices[i].data ()); + } + + const auto lagrange_nodes = get_lagrange_nodes (lagrange_order); + for (const auto &ref_node : lagrange_nodes) { + std::array phys_point; + if constexpr (TShape == T8_ECLASS_TRIANGLE) { + const double w0 = 1.0 - ref_node[0] - ref_node[1]; + const double w1 = ref_node[0]; + const double w2 = ref_node[1]; + + for (auto d = 0; d < 3; ++d) + phys_point[d] = w0 * vertices[0][d] + w1 * vertices[1][d] + w2 * vertices[2][d]; + } + else { + phys_point = map_cartesian (ref_node, vertices); + } + all_points.push_back (phys_point); + file << " " << phys_point[0] << " " << phys_point[1] << " " << phys_point[2] << "\n"; + } + } + } + + file << " \n"; + file << " \n"; + + // Write cells + file << " \n"; + file << " \n"; + + for (auto elem_idx = 0; elem_idx < num_local_elements; ++elem_idx) { + file << " "; + + const auto base_idx = elem_idx * num_nodes_per_elem; + for (auto i = 0; i < num_nodes_per_elem; ++i) + file << (base_idx + i) << " "; + file << "\n"; + } + + file << " \n"; + file << " \n"; + + for (auto elem_idx = 1; elem_idx <= num_local_elements; ++elem_idx) + file << " " << (elem_idx * num_nodes_per_elem) << "\n"; + + file << " \n"; + file << " \n"; + + for (auto elem_idx = 0; elem_idx < num_local_elements; ++elem_idx) + file << " " << vtk_cell_type << "\n"; + + file << " \n"; + file << " \n"; + + // Write cell data (HigherOrderDegrees and Level) + file << " \n"; + file << " \n"; + + // Per-axis degree: the shape's DIM axes carry lagrange_order, the rest 1. + for (auto elem_idx = 0; elem_idx < num_local_elements; ++elem_idx) { + file << " "; + + for (auto d = 0; d < 3; ++d) + file << (d < shape_traits::DIM ? lagrange_order : 1) << (d < 2 ? " " : "\n"); + } + + file << " \n"; + + // Write refinement level for each element + file << " \n"; + + for (auto tree_idx = 0; tree_idx < num_local_trees; ++tree_idx) { + const auto num_elem_in_tree = t8_forest_get_tree_num_leaf_elements (forest, tree_idx); + const auto tree_class = t8_forest_get_tree_class (forest, tree_idx); + + for (auto elem_in_tree = 0; elem_in_tree < num_elem_in_tree; ++elem_in_tree) { + const auto *element = t8_forest_get_leaf_element_in_tree (forest, tree_idx, elem_in_tree); + const int level = scheme->element_get_level (tree_class, element); + file << " " << level << "\n"; + } + } + + file << " \n"; + + // Write the owning MPI rank for each element (partition visualization) + file << " \n"; + for (auto e = 0; e < num_local_elements; ++e) + file << " " << mpirank << "\n"; + + file << " \n"; + + file << " \n"; + + // Write point data (solution values) + file << " \n"; + + for (auto u = 0; u < U_DIM; ++u) { + file << R"( \n"; + + for (auto tree_idx = 0; tree_idx < num_local_trees; ++tree_idx) { + const auto num_elem_in_tree = t8_forest_get_tree_num_leaf_elements (forest, tree_idx); + const auto base_tree = t8_forest_global_tree_id (forest, tree_idx); + + for (auto elem_in_tree = 0; elem_in_tree < num_elem_in_tree; ++elem_in_tree) { + const auto *element = t8_forest_get_leaf_element_in_tree (forest, tree_idx, elem_in_tree); + + // Get LMI for this element + const auto lmi = typename TMultiscale::levelmultiindex (base_tree, element, scheme); + + const auto *data = lmi_map->find (lmi); + if (!data) { + // Element not in map, output zeros + for (auto i = 0; i < num_nodes_per_elem; ++i) + file << " 0.0\n"; + continue; + } + + const auto lagrange_nodes = get_lagrange_nodes (lagrange_order); + for (const auto &ref_node : lagrange_nodes) + file << " " << mra.evaluate_reference (*data, ref_node)[u] << "\n"; + } + } + + file << " \n"; + } + + file << " \n"; + + // Write footer + write_vtk_footer (file); + + file.close (); + + if (mpisize > 1 && mpirank == 0) + write_vtk_master (prefix, mpisize, U_DIM); + + t8_debugf ("Wrote VTK file: %s\n", filename.c_str ()); +} + +} // namespace t8_mra + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/num/basis/basis.hxx b/src/t8_mra/num/basis/basis.hxx new file mode 100644 index 0000000000..7fee823fce --- /dev/null +++ b/src/t8_mra/num/basis/basis.hxx @@ -0,0 +1,53 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include +#include +#include + +#include + +#include "t8_mra/core/shape_traits.hxx" + +namespace t8_mra +{ + +// ============================================================================ +// Reference-element polynomial basis +// ============================================================================ +// One specialization of basis per element shape is the single place +// that defines a shape's function space (in num/shape/). Everything that +// evaluates the basis (projection in dg_basis, VTK output) goes through this +// interface. + +/// Minimal interface every basis specialization provides. +template +concept reference_basis = requires (std::array (TBasis::DIM)> x, double vol) { + { TBasis::DIM } -> std::convertible_to; + { TBasis::DOF } -> std::convertible_to; + { TBasis::eval (x) } -> std::same_as (TBasis::DOF)>>; + { TBasis::normalization (vol) } -> std::convertible_to; +}; + +template +struct basis; + +/// Physical cell mean of a modal field (only the zeroth mode survives). +template +[[nodiscard]] inline double +cell_mean (std::span coeffs, double vol) +{ + using basis_t = basis; + static const double phi0 = basis_t::eval ({})[0]; + return basis_t::normalization (vol) * phi0 * coeffs[0]; +} + +} // namespace t8_mra + +// Per-shape specializations (defined after the primary template). +#include "t8_mra/num/shape/cartesian.hxx" +#include "t8_mra/num/shape/triangle.hxx" + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/num/basis/dubiner.cxx b/src/t8_mra/num/basis/dubiner.cxx new file mode 100644 index 0000000000..5bf2ec44b5 --- /dev/null +++ b/src/t8_mra/num/basis/dubiner.cxx @@ -0,0 +1,29 @@ +#ifdef T8_ENABLE_MRA + +#include "t8_mra/num/basis/dubiner.hxx" + +namespace t8_mra +{ +double +jacobi (int n, double alpha, double beta, double x) +{ + if (n == 0) + return 1.0; + double p_prev = 1.0; + double p = 0.5 * (alpha - beta) + 0.5 * (alpha + beta + 2.0) * x; + for (int k = 2; k <= n; ++k) { + const double c = 2.0 * k + alpha + beta; + const double a1 = 2.0 * k * (k + alpha + beta) * (c - 2.0); + const double a2 = (c - 1.0) * (alpha * alpha - beta * beta); + const double a3 = (c - 1.0) * c * (c - 2.0); + const double a4 = 2.0 * (k + alpha - 1.0) * (k + beta - 1.0) * c; + const double p_next = ((a2 + a3 * x) * p - a4 * p_prev) / a1; + p_prev = p; + p = p_next; + } + return p; +} + +} // namespace t8_mra + +#endif diff --git a/src/t8_mra/num/basis/dubiner.hxx b/src/t8_mra/num/basis/dubiner.hxx new file mode 100644 index 0000000000..1f8fd9fa8c --- /dev/null +++ b/src/t8_mra/num/basis/dubiner.hxx @@ -0,0 +1,107 @@ +#pragma once +#ifdef T8_ENABLE_MRA + +#include +#include + +namespace t8_mra +{ +/// Jacobi polynomial P_n^{(alpha,beta)}(x) by three-term recurrence. +[[nodiscard]] double +jacobi (int n, double alpha, double beta, double x); + +/// Derivative d/dx P_n^{(alpha,beta)}(x) = (n+alpha+beta+1)/2 P_{n-1}^{(alpha+1,beta+1)}(x). +[[nodiscard]] inline double +jacobi_deriv (int n, double alpha, double beta, double x) +{ + if (n == 0) + return 0.0; + return 0.5 * (n + alpha + beta + 1.0) * jacobi (n - 1, alpha + 1.0, beta + 1.0, x); +} + +namespace detail +{ +/// Total degree d of linear basis index i (smallest d with (d+1)(d+2)/2 > i). +[[nodiscard]] constexpr int +dubiner_degree (int i) +{ + int d = 0; + while ((d + 1) * (d + 2) / 2 <= i) + ++d; + return d; +} + +/// c^N for small compile-time N +template +[[nodiscard]] constexpr double +power (double c) +{ + double r = 1.0; + for (int k = 0; k < N; ++k) + r *= c; + return r; +} +} // namespace detail + +/// I-th orthonormal Dubiner scaling function on the reference triangle (area +/// 1/2), via Jacobi recurrence; valid for any I. tau1, tau2 in [0,1]. +template +[[nodiscard]] double +scaling_function (double tau1, double tau2) +{ + constexpr int d = detail::dubiner_degree (I); + constexpr int p = I - d * (d + 1) / 2; + constexpr int q = d - p; + + // Collapsed coordinates. At the apex (tau2 -> 1) the c^p factor kills every + // p>=1 term. + const double c = 1.0 - tau2; + const double a = c < 1e-12 ? -1.0 : 1.0 - 2.0 * tau1 / c; + const double b = 2.0 * tau2 - 1.0; + const double norm = std::sqrt (2.0 * (2 * p + 1) * (p + q + 1)); + + return norm * jacobi (p, 0.0, 0.0, a) * detail::power

(c) * jacobi (q, 2.0 * p + 1.0, 0.0, b); +} + +/// Reference gradient {d/dtau1, d/dtau2} of the I-th Dubiner scaling function. +/// Evaluated at interior (Dunavant) points; the collapsed-coordinate gradient +/// is singular only at the apex (c -> 0, p == 1), guarded by clamping c. +template +[[nodiscard]] std::array +scaling_function_gradient (double tau1, double tau2) +{ + constexpr int d = detail::dubiner_degree (I); + constexpr int p = I - d * (d + 1) / 2; + constexpr int q = d - p; + + const double norm = std::sqrt (2.0 * (2 * p + 1) * (p + q + 1)); + const double c = 1.0 - tau2; + const double cc = c < 1e-12 ? 1e-12 : c; + const double a = 1.0 - 2.0 * tau1 / cc; + const double b = 2.0 * tau2 - 1.0; + + const double pa = jacobi (p, 0.0, 0.0, a); + const double pb = jacobi (q, 2.0 * p + 1.0, 0.0, b); + const double pad = jacobi_deriv (p, 0.0, 0.0, a); + const double pbd = jacobi_deriv (q, 2.0 * p + 1.0, 0.0, b); + + double dt1 = 0.0; + double dt2 = norm * 2.0 * pa * pbd * detail::power

(cc); // term from d(P_q(b))/dtau2 + + // The d/dtau1 path and the c-power derivatives only contribute for p >= 1 + // (for p == 0 the Jacobi-in-a derivative pad is zero anyway). + if constexpr (p >= 1) { + const double cpm1 = detail::power

(cc); + dt1 = norm * pad * (-2.0) * cpm1 * pb; // d a/d tau1 = -2/c + dt2 += norm * (-static_cast (p)) * pa * pb * cpm1; // d(c^p)/d tau2 + if constexpr (p >= 2) + dt2 += norm * (-2.0) * tau1 * pad * pb * detail::power

(cc); // d a/d tau2 = -2 tau1/c^2 + else + dt2 += norm * (-2.0) * tau1 * pad * pb / cc; // p == 1: c^{p-2} = 1/c + } + + return { dt1, dt2 }; +} + +} // namespace t8_mra +#endif diff --git a/src/t8_mra/num/basis/legendre.hxx b/src/t8_mra/num/basis/legendre.hxx new file mode 100644 index 0000000000..8f1c95ed76 --- /dev/null +++ b/src/t8_mra/num/basis/legendre.hxx @@ -0,0 +1,62 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include + +#include + +namespace t8_mra +{ + +/** + * @brief The p-th Legendre polynomial at x, shifted to [0,1] and L2-normalized. + * + * Shift x_std = 2x-1 and multiply by sqrt(2p+1) for orthonormality on [0,1]. + * + * @param x Point in [0,1]. + * @param p Polynomial degree. + */ +[[nodiscard]] inline double +phi_1d (double x, int p) +{ + // Transform x from [0,1] to [-1,1] for standard Legendre polynomials + double x_std = 2.0 * x - 1.0; + + // Evaluate Legendre polynomial using GSL + double leg_value = gsl_sf_legendre_Pl (p, x_std); + + // Apply L2-normalization for [0,1] interval: sqrt(2*p + 1) + return leg_value * std::sqrt (2.0 * p + 1.0); +} + +/** + * @brief Derivative of the p-th normalized Legendre polynomial at x on [0,1]. + * + * @tparam P number of 1D modes (degrees 0..P-1); sizes the GSL scratch exactly. + * @param p Polynomial degree in [0, P). + * + * GSL writes degrees 0..p; the chain-rule factor 2 (from x_std = 2x-1) and the + * sqrt(2p+1) normalization are applied to the requested degree. + */ +template +[[nodiscard]] inline double +phi_prime_1d (double x, int p) +{ + const auto x_std = 2.0 * x - 1.0; + + // Derivative of the constant mode is zero (and keeps p+1 <= P for p > 0). + if (p == 0) + return 0.0; + + std::array leg_array; + std::array deriv_array; + gsl_sf_legendre_Pl_deriv_array (p, x_std, leg_array.data (), deriv_array.data ()); + + return 2.0 * deriv_array[p] * std::sqrt (2.0 * p + 1.0); +} + +} // namespace t8_mra + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/num/cell_geometry.hxx b/src/t8_mra/num/cell_geometry.hxx new file mode 100644 index 0000000000..5ba59ef56c --- /dev/null +++ b/src/t8_mra/num/cell_geometry.hxx @@ -0,0 +1,24 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include + +namespace t8_mra +{ + +/// Reference-coordinate containment slack (boundary points, affine round-off). +inline constexpr double reference_cell_tol = 1e-9; + +/// Cached affine geometry of one leaf. One specialization per shape (num/shape/); +/// trivially copyable. +template +struct cell_geometry; + +} // namespace t8_mra + +// Per-shape specializations (defined after the primary template). +#include "t8_mra/num/shape/cartesian.hxx" +#include "t8_mra/num/shape/triangle.hxx" + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/num/dg_basis.hxx b/src/t8_mra/num/dg_basis.hxx new file mode 100644 index 0000000000..461d0648da --- /dev/null +++ b/src/t8_mra/num/dg_basis.hxx @@ -0,0 +1,100 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include + +#include "t8_eclass/t8_eclass.h" + +#include "t8_mra/core/shape_traits.hxx" +#include "t8_mra/num/basis/basis.hxx" +#include "t8_mra/num/quadrature/quadrature.hxx" + +namespace t8_mra +{ + +/// Primary template left undefined: only cartesian shapes and the triangle are +/// supported (specializations below). Any other shape is a compile error. +template +struct dg_basis_base; + +template +struct dg_basis_base>> +{ + static constexpr unsigned int DIM = TShape == T8_ECLASS_LINE ? 1 : (TShape == T8_ECLASS_QUAD ? 2 : 3); + static constexpr t8_eclass Shape = TShape; + + // Reference Gauss-Legendre tensor rule. + quadrature quad; + + dg_basis_base () = default; + + explicit dg_basis_base (int num_quad_points_1d): quad (num_quad_points_1d) + { + } +}; + +template <> +struct dg_basis_base +{ + static constexpr unsigned int DIM = 2; + static constexpr t8_eclass Shape = T8_ECLASS_TRIANGLE; + + quadrature quad; + + dg_basis_base () = default; + + explicit dg_basis_base (int dunavant_rule): quad (dunavant_rule) + { + } +}; + +template +class dg_basis: public dg_basis_base { + using Element = TElement; + using Base = dg_basis_base; + + static constexpr unsigned int DIM = Element::DIM; + static constexpr auto Shape = TElement::Shape; + + static constexpr unsigned int P_DIM = Element::P_DIM; + static constexpr unsigned int DOF = Element::DOF; + static constexpr unsigned int W_DOF = Element::W_DOF; + + using basis_t = basis; + + public: + dg_basis () = default; + + // Constructor for triangular elements + explicit dg_basis (int _dunavant_rule) + requires (Shape == T8_ECLASS_TRIANGLE) + : Base (_dunavant_rule) + { + } + + // Constructor for cartesian elements (LINE, QUAD, HEX) + explicit dg_basis (int _num_quad_points_1d) + requires is_cartesian + : Base (_num_quad_points_1d) + { + } + + /// All basis function values at a reference point. + [[nodiscard]] std::array + basis_value (const std::array &x_ref) const + { + return basis_t::eval (x_ref); + } + + /// grad[dir][i] = d(phi_i)/dx_dir at a reference point. + [[nodiscard]] std::array, DIM> + basis_gradient (const std::array &x_ref) const + { + return basis_t::eval_gradient (x_ref); + } +}; + +} // namespace t8_mra +#endif diff --git a/src/t8_mra/num/geometry.hxx b/src/t8_mra/num/geometry.hxx new file mode 100644 index 0000000000..4b5d2543b1 --- /dev/null +++ b/src/t8_mra/num/geometry.hxx @@ -0,0 +1,72 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include + +namespace t8_mra +{ + +/// Affine map from reference [0,1] to physical [xL, xR]. +[[nodiscard]] constexpr double +deref_1d (double x_ref, double xL, double xR) noexcept +{ + return x_ref * (xR - xL) + xL; +} + +/// Dimension-wise affine map [0,1]^DIM -> physical cartesian cell. +template +[[nodiscard]] constexpr std::array +deref (const std::array &x_ref, const std::array &vertices_min, + const std::array &vertices_max) noexcept +{ + std::array x = {}; + for (auto d = 0u; d < DIM; ++d) + x[d] = deref_1d (x_ref[d], vertices_min[d], vertices_max[d]); + + return x; +} + +/// Min/max corners of an axis-aligned cartesian cell from t8code vertices. The +/// vertex permutation applied in dg/cartesian.hxx puts the lower corner at +/// index 0 and the upper corner at the last index (2/3/7 for LINE/QUAD/HEX). +template +inline void +extract_cartesian_vertices (const std::array, N> &physical_vertices, + std::array &vertices_min, std::array &vertices_max) noexcept +{ + constexpr int max_vertex = (DIM == 1) ? 1 : (DIM == 2) ? 2 : 7; + + for (auto d = 0u; d < DIM; ++d) { + vertices_min[d] = physical_vertices[0][d]; + vertices_max[d] = physical_vertices[max_vertex][d]; + } +} + +/// Maps the flattened reference quadrature points ([x0,y0,..., x1,y1,...]) to +/// the physical cartesian cell. +template +[[nodiscard]] inline std::vector +transform_quad_points (const std::vector &ref_quad_points, size_t num_points, + const std::array &vertices_min, const std::array &vertices_max) +{ + std::vector phys_quad_points (DIM * num_points); + + for (auto i = 0u; i < num_points; ++i) { + std::array x_ref; + for (auto d = 0u; d < DIM; ++d) + x_ref[d] = ref_quad_points[DIM * i + d]; + + const std::array x_phys = deref (x_ref, vertices_min, vertices_max); + + for (auto d = 0u; d < DIM; ++d) + phys_quad_points[DIM * i + d] = x_phys[d]; + } + + return phys_quad_points; +} + +} // namespace t8_mra + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/num/mask_coefficients.hxx b/src/t8_mra/num/mask_coefficients.hxx new file mode 100644 index 0000000000..38bd2eacd8 --- /dev/null +++ b/src/t8_mra/num/mask_coefficients.hxx @@ -0,0 +1,167 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace t8_mra +{ + +// ============================================================================ +// Two-scale low-pass mask coefficients +// ============================================================================ +// One routine for every shape: the parent -> child-k prolongation matrix +// M_k(i, j) = norm * ∫_ref φ_i(ξ) φ_j(Φ_k ξ) dξ +// with φ = basis (the reference function space) and Φ_k the affine +// map of the reference element onto child k of a uniform refinement. Row i is a +// child dof, column j a parent dof. + +/// Affine map ξ -> A ξ + b on the reference element. +template +struct affine_map +{ + std::array, DIM> A {}; + std::array b {}; + + [[nodiscard]] std::array + operator() (const std::array &xi) const + { + std::array out {}; + for (auto r = 0; r < DIM; ++r) { + out[r] = b[r]; + for (auto c = 0; c < DIM; ++c) + out[r] += A[r][c] * xi[c]; + } + return out; + } +}; + +/// Cartesian children: 2^DIM axis-aligned half-cells, Φ_k(ξ) = (s_k + ξ)/2. +template + requires is_cartesian +[[nodiscard]] auto +child_maps () +{ + constexpr int DIM = shape_traits::DIM; + constexpr int NC = shape_traits::NUM_CHILDREN; + + std::array, NC> maps {}; + for (auto k = 0; k < NC; ++k) { + for (auto r = 0; r < DIM; ++r) { + maps[k].A[r][r] = 0.5; + maps[k].b[r] = 0.5 * ((k >> r) & 1); + } + } + return maps; +} + +/// Triangle children: red refinement into 3 corner triangles + 1 inverted +/// centre; per-child vertex order fixes the two-scale convention. +template + requires (TShape == T8_ECLASS_TRIANGLE) +[[nodiscard]] auto +child_maps () +{ + using vertex = std::array; + constexpr vertex p0 { 0.0, 0.0 }; + constexpr vertex p1 { 1.0, 0.0 }; + constexpr vertex p2 { 0.0, 1.0 }; + + constexpr vertex m01 { 0.5, 0.0 }; + constexpr vertex m02 { 0.0, 0.5 }; + constexpr vertex m12 { 0.5, 0.5 }; + + const std::array, 4> verts { { + { m01, m12, m02 }, // centre (inverted) + { m01, p1, m12 }, + { m12, p2, m02 }, + { m02, p0, m01 }, + } }; + + std::array, 4> maps {}; + for (auto k = 0; k < 4; ++k) { + const auto &v = verts[k]; + maps[k].b = v[0]; + + for (auto r = 0; r < 2; ++r) { + maps[k].A[r][0] = v[1][r] - v[0][r]; + maps[k].A[r][1] = v[2][r] - v[0][r]; + } + } + return maps; +} + +/// Per-shape mask normalization. Specialize for a new shape. +template +struct mask_policy; + +/// Cartesian basis is orthonormal on the unit cell (vol 1). +template + requires is_cartesian +struct mask_policy +{ + static constexpr double norm = 1.0; +}; + +/// Triangle factor 1/4 = 1/2 (two-scale definition) * 1/2 (reference area). +template <> +struct mask_policy +{ + static constexpr double norm = 0.25; +}; + +/// Compute the NUM_CHILDREN two-scale masks for shape TShape at order P. +template +void +compute_mask (std::vector &mask) +{ + using basis_t = basis; + constexpr int DIM = basis_t::DIM; + constexpr int DOF = basis_t::DOF; + constexpr int NC = shape_traits::NUM_CHILDREN; + + mask.assign (NC, t8_mra::mat { DOF, DOF }); + + const auto children = child_maps (); + // Integrand child_val * parent_val has degree 2(P-1); 2P integrates it exactly. + const quadrature quad (quadrature::rule_for_degree (2 * P)); + + auto wsum = 0.0; + for (auto q = 0u; q < quad.num_points; ++q) + wsum += quad.weights[q]; + const auto scale = mask_policy::norm / wsum; + + for (auto k = 0; k < NC; ++k) { + for (auto q = 0u; q < quad.num_points; ++q) { + std::array xi {}; + + for (auto d = 0; d < DIM; ++d) + xi[d] = quad.points[DIM * q + d]; + + const auto child_val = basis_t::eval (xi); + const auto parent_val = basis_t::eval (children[k](xi)); + const double w = quad.weights[q]; + + for (auto i = 0; i < DOF; ++i) + for (auto j = 0; j < DOF; ++j) + mask[k](i, j) += w * child_val[i] * parent_val[j]; + } + + for (auto i = 0; i < DOF; ++i) + for (auto j = 0; j < DOF; ++j) + mask[k](i, j) *= scale; + } +} + +} // namespace t8_mra + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/num/mat.hxx b/src/t8_mra/num/mat.hxx new file mode 100644 index 0000000000..e315c430d8 --- /dev/null +++ b/src/t8_mra/num/mat.hxx @@ -0,0 +1,180 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include +#include +#include +#include + +namespace t8_mra +{ + +/// Dense row-major matrix; see lu_factors / lu_solve for the pivoted LU solve. +class mat { + std::vector data; + size_t num_rows = 0u; + size_t num_cols = 0u; + + public: + mat () = default; + mat (size_t _rows, size_t _cols): data (_rows * _cols, {}), num_rows (_rows), num_cols (_cols) + { + } + + mat (size_t _rows, size_t _cols, std::initializer_list l): data (l), num_rows (_rows), num_cols (_cols) + { + if (l.size () != _rows * _cols) + throw std::out_of_range ("number elements in t8_mra::util::mat does not fit to number columns " + "and number rows"); + } + + mat (const mat &) = default; + mat & + operator= (const mat &) = default; + mat (mat &&) = default; + mat & + operator= (mat &&) = default; + + mat & + operator= (const std::initializer_list &l) + { + if (l.size () != num_rows * num_cols) + throw std::out_of_range ("number elements in t8_mra::util::mat does not fit to number columns " + "and number rows"); + + std::copy_n (l.begin (), l.size (), data.begin ()); + + return *this; + } + + double & + operator() (size_t i, size_t j); + [[nodiscard]] double + operator() (size_t i, size_t j) const; + + mat & + operator= (double v); + + void + resize (size_t _m, size_t _n); + + [[nodiscard]] size_t + rows () const noexcept; + [[nodiscard]] size_t + cols () const noexcept; +}; + +inline double & +mat::operator() (size_t i, size_t j) +{ +#if T8_ENABLE_DEBUG + if (i >= num_rows || j >= num_cols) + throw std::out_of_range ("indices in t8_mra::util::mat::operator() is out of range"); +#endif + return data[num_cols * i + j]; +} + +inline double +mat::operator() (size_t i, size_t j) const +{ +#if T8_ENABLE_DEBUG + if (i >= num_rows || j >= num_cols) + throw std::out_of_range ("indices in t8_mra::util::mat::operator() is out of range"); +#endif + return data[num_cols * i + j]; +} + +inline void +mat::resize (size_t _rows, size_t _cols) +{ + data.clear (); + num_rows = _rows; + num_cols = _cols; + data.resize (_rows * _cols); +} + +inline size_t +mat::rows () const noexcept +{ + return num_rows; +} +inline size_t +mat::cols () const noexcept +{ + return num_cols; +} + +/// Matrix is saved as A = (L - E_n) + U +/// below diagonal: L +/// Remaining matrix: U +inline void +lu_factors (mat &A, std::vector &p) +{ + if (A.rows () != A.cols ()) + throw std::logic_error ("Matrix in t8_mra::util::lr_factor is not a square matrix"); + + const auto n = A.rows (); + p.resize (n); + + for (auto i = 0u; i < n; ++i) + p[i] = i; + + for (auto j = 0u; j < n; j++) { + auto Aj_max = 0.0; + auto piv = j; + + for (auto k = j; k < n; k++) { + const auto Ap = std::abs (A (k, j)); + + if (Ap > Aj_max) { + Aj_max = Ap; + piv = k; + } + } + + if (piv != j) { + std::swap (p[piv], p[j]); + for (auto k = 0u; k < n; k++) + std::swap (A (piv, k), A (j, k)); + } + + for (auto i = j + 1; i < n; i++) { + A (i, j) /= A (j, j); + + for (auto k = j + 1; k < n; k++) + A (i, k) -= A (i, j) * A (j, k); + } + } +} + +inline void +lu_solve (const mat &A, const std::vector &p, std::span x) +{ + if (A.rows () != A.cols ()) + throw std::logic_error ("Matrix in t8_mra::util::lr_solve is not a square matrix"); + if (A.rows () != p.size ()) + throw std::logic_error ("Permutation vector in t8_mra::util::lr_solve does not fit"); + if (A.rows () != x.size ()) + throw std::logic_error ("Solution vector in t8_mra::util::lr_solve does not fit"); + + const auto n = A.rows (); + + const std::vector b (x.begin (), x.end ()); + for (auto i = 0u; i < n; ++i) { + x[i] = b[p[i]]; + for (auto k = 0u; k < i; ++k) + x[i] -= A (i, k) * x[k]; + } + + for (int i = n - 1; i >= 0; --i) { + for (auto k = static_cast (i + 1); k < n; ++k) + x[i] -= A (i, k) * x[k]; + x[i] /= A (i, i); + } +} + +} // namespace t8_mra + +#endif diff --git a/src/t8_mra/num/nodal_to_modal.hxx b/src/t8_mra/num/nodal_to_modal.hxx new file mode 100644 index 0000000000..a9dde6c94d --- /dev/null +++ b/src/t8_mra/num/nodal_to_modal.hxx @@ -0,0 +1,132 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include +#include + +#include + +#include "t8_mra/core/shape_traits.hxx" +#include "t8_mra/num/basis/basis.hxx" +#include "t8_mra/num/mat.hxx" + +namespace t8_mra +{ + +/** + * @brief Converts nodal DG data to the modal coefficients MRA stores. + * + * Given a nodal value n_j = u(x_j) at reference nodes x_j, the modal + * coefficients solve V m = n with the Vandermonde V_ji = phi_i(x_j). The + * cartesian basis is orthonormal with unit normalization on every cell, so V is + * cell-independent: it is built and LU-factored once at construction, then each + * per-cell conversion is a pair of triangular solves per component. + * + * Nodes must be DOF distinct reference points making V nonsingular (e.g. a + * tensor Gauss-Lobatto / equispaced nodal set of order P). Nodal and modal + * buffers are component-major: index u*DOF + j, matching element_data u_coeffs. + */ +template + requires is_cartesian +class nodal_to_modal { + public: + static constexpr unsigned int DIM = shape_traits::DIM; + static constexpr unsigned int DOF = shape_traits::dof (P); + + explicit nodal_to_modal (const std::array, DOF> &nodes): vandermonde (DOF, DOF), perm (DOF) + { + for (auto j = 0u; j < DOF; ++j) { + const auto phi = basis::eval (nodes[j]); + for (auto i = 0u; i < DOF; ++i) + vandermonde (j, i) = phi[i]; + } + lu_factors (vandermonde, perm); + } + + /// Convert one cell's nodal values (U*DOF) into modal coeffs (U*DOF). + void + operator() (std::span nodal, std::span modal) const + { + std::array rhs; + for (auto u = 0u; u < U; ++u) { + const auto off = u * DOF; + + for (auto j = 0u; j < DOF; ++j) + rhs[j] = nodal[off + j]; + + lu_solve (vandermonde, perm, rhs); + + for (auto i = 0u; i < DOF; ++i) + modal[off + i] = rhs[i]; + } + } + + [[nodiscard]] std::array + operator() (std::span nodal) const + { + std::array modal; + (*this) (nodal, modal); + + return modal; + } + + private: + mat vandermonde; // LU-factored Vandermonde phi_i(x_j) + std::vector perm; // pivot permutation +}; + +/** + * @brief Reconstructs nodal DG values from modal coefficients (inverse of + * nodal_to_modal). + * + * n_j = sum_i m_i phi_i(x_j), a matvec against the Vandermonde built from the + * reference `nodes`; phi_i(x_j) is evaluated once at construction. Cartesian + * only, buffers component-major (index u*DOF + j). + */ +template + requires is_cartesian +class modal_to_nodal { + public: + static constexpr unsigned int DIM = shape_traits::DIM; + static constexpr unsigned int DOF = shape_traits::dof (P); + + explicit modal_to_nodal (const std::array, DOF> &nodes) + { + for (auto j = 0u; j < DOF; ++j) + phi_at_node[j] = basis::eval (nodes[j]); + } + + void + operator() (std::span modal, std::span nodal) const + { + for (auto u = 0u; u < U; ++u) { + const auto off = u * DOF; + + for (auto j = 0u; j < DOF; ++j) { + auto v = 0.0; + for (auto i = 0u; i < DOF; ++i) + v += modal[off + i] * phi_at_node[j][i]; + + nodal[off + j] = v; + } + } + } + + [[nodiscard]] std::array + operator() (std::span modal) const + { + std::array nodal = {}; + (*this) (modal, nodal); + + return nodal; + } + + private: + std::array, DOF> phi_at_node; // phi_i(x_j) +}; + +} // namespace t8_mra + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/num/quadrature/dunavant.cxx b/src/t8_mra/num/quadrature/dunavant.cxx new file mode 100644 index 0000000000..8b54c5f7bc --- /dev/null +++ b/src/t8_mra/num/quadrature/dunavant.cxx @@ -0,0 +1,71 @@ +#ifdef T8_ENABLE_MRA + +#include +#include + +#include "t8_mra/num/quadrature/dunavant.hxx" +#include "t8_mra/num/quadrature/dunavant_table.hxx" + +namespace t8_mra +{ + +dunavant_quadrature +dunavant_rule (int rule) +{ + if (rule < 1 || rule > 20) + throw std::out_of_range ("dunavant_rule: rule must be in [1, 20]"); + + dunavant_quadrature q; + + const auto add = [&q] (double x, double y, double w) { + q.points.push_back (x); + q.points.push_back (y); + q.weights.push_back (w); + }; + + // Each orbit expands into mult points by cyclic permutation of its + // barycentric coordinates (x = b[k], y = b[k+1], third coord implied). + for (const auto &o : dunavant_table::rule (rule)) { + const auto &b = o.bary; + switch (o.mult) { + case 1: + add (b[0], b[1], o.weight); + break; + case 3: + for (int k = 0; k < 3; ++k) + add (b[k % 3], b[(k + 1) % 3], o.weight); + break; + case 6: + for (int k = 0; k < 3; ++k) + add (b[k % 3], b[(k + 1) % 3], o.weight); + for (int k = 0; k < 3; ++k) + add (b[(k + 1) % 3], b[k % 3], o.weight); + break; + default: + throw std::logic_error ("dunavant_rule: invalid orbit multiplicity"); + } + } + + return q; +} + +std::vector +reference_to_physical_t3 (std::span tri, std::span ref) +{ + const std::size_t n = ref.size () / 2; + std::vector phy (2 * n); + + // Affine map from barycentric (1-r0-r1, r0, r1) to the physical triangle. + for (std::size_t j = 0; j < n; ++j) { + const double r0 = ref[2 * j]; + const double r1 = ref[2 * j + 1]; + for (int i = 0; i < 2; ++i) + phy[2 * j + i] = tri[i] * (1.0 - r0 - r1) + tri[2 + i] * r0 + tri[4 + i] * r1; + } + + return phy; +} + +} // namespace t8_mra + +#endif diff --git a/src/t8_mra/num/quadrature/dunavant.hxx b/src/t8_mra/num/quadrature/dunavant.hxx new file mode 100644 index 0000000000..486862e597 --- /dev/null +++ b/src/t8_mra/num/quadrature/dunavant.hxx @@ -0,0 +1,39 @@ +/** + * @file + * @brief Dunavant quadrature rules over the reference triangle. + * Coefficients from David Dunavant, "High Degree Efficient Symmetrical + * Gaussian Quadrature Rules for the Triangle", IJNME 21 (1985), 1129-1148. + * Original C tables by John Burkardt; the data lives in dunavant_table.hxx. + */ + +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include + +namespace t8_mra +{ + +/// A reference-triangle quadrature rule: points flattened as [x0,y0, x1,y1, ...] +/// with one weight per point. +struct dunavant_quadrature +{ + std::vector points; + std::vector weights; +}; + +/// Expand Dunavant rule `rule` (1..20) into its full point/weight set. +[[nodiscard]] dunavant_quadrature +dunavant_rule (int rule); + +/// Map reference-triangle points to physical space. tri holds the three +/// vertices [x0,y0, x1,y1, x2,y2]; ref holds points [x0,y0, ...]; the returned +/// vector holds the physical points in the same flattened layout. +[[nodiscard]] std::vector +reference_to_physical_t3 (std::span tri, std::span ref); + +} // namespace t8_mra + +#endif diff --git a/src/t8_mra/num/quadrature/dunavant_table.hxx b/src/t8_mra/num/quadrature/dunavant_table.hxx new file mode 100644 index 0000000000..4dde8a8ed2 --- /dev/null +++ b/src/t8_mra/num/quadrature/dunavant_table.hxx @@ -0,0 +1,254 @@ +#pragma once + +// GENERATED from the original Burkardt Dunavant tables (do not edit by hand). +// One orbit = barycentric coords (b0,b1,b2), suborder weight, symmetry +// multiplicity (1 centroid, 3 edge-type, 6 generic). See dunavant.cxx. + +#ifdef T8_ENABLE_MRA + +#include +#include + +namespace t8_mra::dunavant_table +{ + +struct orbit +{ + std::array bary; + double weight; + int mult; +}; + +inline constexpr std::array rule_01 = { { + { { { 0.333333333333333, 0.333333333333333, 0.333333333333333 } }, 1.000000000000000, 1 }, +} }; + +inline constexpr std::array rule_02 = { { + { { { 0.666666666666667, 0.166666666666667, 0.166666666666667 } }, 0.333333333333333, 3 }, +} }; + +inline constexpr std::array rule_03 = { { + { { { 0.333333333333333, 0.333333333333333, 0.333333333333333 } }, -0.562500000000000, 1 }, + { { { 0.600000000000000, 0.200000000000000, 0.200000000000000 } }, 0.520833333333333, 3 }, +} }; + +inline constexpr std::array rule_04 = { { + { { { 0.108103018168070, 0.445948490915965, 0.445948490915965 } }, 0.223381589678011, 3 }, + { { { 0.816847572980459, 0.091576213509771, 0.091576213509771 } }, 0.109951743655322, 3 }, +} }; + +inline constexpr std::array rule_05 = { { + { { { 0.333333333333333, 0.333333333333333, 0.333333333333333 } }, 0.225000000000000, 1 }, + { { { 0.059715871789770, 0.470142064105115, 0.470142064105115 } }, 0.132394152788506, 3 }, + { { { 0.797426985353087, 0.101286507323456, 0.101286507323456 } }, 0.125939180544827, 3 }, +} }; + +inline constexpr std::array rule_06 = { { + { { { 0.501426509658179, 0.249286745170910, 0.249286745170910 } }, 0.116786275726379, 3 }, + { { { 0.873821971016996, 0.063089014491502, 0.063089014491502 } }, 0.050844906370207, 3 }, + { { { 0.053145049844817, 0.310352451033784, 0.636502499121399 } }, 0.082851075618374, 6 }, +} }; + +inline constexpr std::array rule_07 = { { + { { { 0.333333333333333, 0.333333333333333, 0.333333333333333 } }, -0.149570044467682, 1 }, + { { { 0.479308067841920, 0.260345966079040, 0.260345966079040 } }, 0.175615257433208, 3 }, + { { { 0.869739794195568, 0.065130102902216, 0.065130102902216 } }, 0.053347235608838, 3 }, + { { { 0.048690315425316, 0.312865496004874, 0.638444188569810 } }, 0.077113760890257, 6 }, +} }; + +inline constexpr std::array rule_08 = { { + { { { 0.333333333333333, 0.333333333333333, 0.333333333333333 } }, 0.144315607677787, 1 }, + { { { 0.081414823414554, 0.459292588292723, 0.459292588292723 } }, 0.095091634267285, 3 }, + { { { 0.658861384496480, 0.170569307751760, 0.170569307751760 } }, 0.103217370534718, 3 }, + { { { 0.898905543365938, 0.050547228317031, 0.050547228317031 } }, 0.032458497623198, 3 }, + { { { 0.008394777409958, 0.263112829634638, 0.728492392955404 } }, 0.027230314174435, 6 }, +} }; + +inline constexpr std::array rule_09 = { { + { { { 0.333333333333333, 0.333333333333333, 0.333333333333333 } }, 0.097135796282799, 1 }, + { { { 0.020634961602525, 0.489682519198738, 0.489682519198738 } }, 0.031334700227139, 3 }, + { { { 0.125820817014127, 0.437089591492937, 0.437089591492937 } }, 0.077827541004774, 3 }, + { { { 0.623592928761935, 0.188203535619033, 0.188203535619033 } }, 0.079647738927210, 3 }, + { { { 0.910540973211095, 0.044729513394453, 0.044729513394453 } }, 0.025577675658698, 3 }, + { { { 0.036838412054736, 0.221962989160766, 0.741198598784498 } }, 0.043283539377289, 6 }, +} }; + +inline constexpr std::array rule_10 = { { + { { { 0.333333333333333, 0.333333333333333, 0.333333333333333 } }, 0.090817990382754, 1 }, + { { { 0.028844733232685, 0.485577633383657, 0.485577633383657 } }, 0.036725957756467, 3 }, + { { { 0.781036849029926, 0.109481575485037, 0.109481575485037 } }, 0.045321059435528, 3 }, + { { { 0.141707219414880, 0.307939838764121, 0.550352941820999 } }, 0.072757916845420, 6 }, + { { { 0.025003534762686, 0.246672560639903, 0.728323904597411 } }, 0.028327242531057, 6 }, + { { { 0.009540815400299, 0.066803251012200, 0.923655933587500 } }, 0.009421666963733, 6 }, +} }; + +inline constexpr std::array rule_11 = { { + { { { -0.069222096541517, 0.534611048270758, 0.534611048270758 } }, 0.000927006328961, 3 }, + { { { 0.202061394068290, 0.398969302965855, 0.398969302965855 } }, 0.077149534914813, 3 }, + { { { 0.593380199137435, 0.203309900431282, 0.203309900431282 } }, 0.059322977380774, 3 }, + { { { 0.761298175434837, 0.119350912282581, 0.119350912282581 } }, 0.036184540503418, 3 }, + { { { 0.935270103777448, 0.032364948111276, 0.032364948111276 } }, 0.013659731002678, 3 }, + { { { 0.050178138310495, 0.356620648261293, 0.593201213428213 } }, 0.052337111962204, 6 }, + { { { 0.021022016536166, 0.171488980304042, 0.807489003159792 } }, 0.020707659639141, 6 }, +} }; + +inline constexpr std::array rule_12 = { { + { { { 0.023565220452390, 0.488217389773805, 0.488217389773805 } }, 0.025731066440455, 3 }, + { { { 0.120551215411079, 0.439724392294460, 0.439724392294460 } }, 0.043692544538038, 3 }, + { { { 0.457579229975768, 0.271210385012116, 0.271210385012116 } }, 0.062858224217885, 3 }, + { { { 0.744847708916828, 0.127576145541586, 0.127576145541586 } }, 0.034796112930709, 3 }, + { { { 0.957365299093579, 0.021317350453210, 0.021317350453210 } }, 0.006166261051559, 3 }, + { { { 0.115343494534698, 0.275713269685514, 0.608943235779788 } }, 0.040371557766381, 6 }, + { { { 0.022838332222257, 0.281325580989940, 0.695836086787803 } }, 0.022356773202303, 6 }, + { { { 0.025734050548330, 0.116251915907597, 0.858014033544073 } }, 0.017316231108659, 6 }, +} }; + +inline constexpr std::array rule_13 = { { + { { { 0.333333333333333, 0.333333333333333, 0.333333333333333 } }, 0.052520923400802, 1 }, + { { { 0.009903630120591, 0.495048184939705, 0.495048184939705 } }, 0.011280145209330, 3 }, + { { { 0.062566729780852, 0.468716635109574, 0.468716635109574 } }, 0.031423518362454, 3 }, + { { { 0.170957326397447, 0.414521336801277, 0.414521336801277 } }, 0.047072502504194, 3 }, + { { { 0.541200855914337, 0.229399572042831, 0.229399572042831 } }, 0.047363586536355, 3 }, + { { { 0.771151009607340, 0.114424495196330, 0.114424495196330 } }, 0.031167529045794, 3 }, + { { { 0.950377217273082, 0.024811391363459, 0.024811391363459 } }, 0.007975771465074, 3 }, + { { { 0.094853828379579, 0.268794997058761, 0.636351174561660 } }, 0.036848402728732, 6 }, + { { { 0.018100773278807, 0.291730066734288, 0.690169159986905 } }, 0.017401463303822, 6 }, + { { { 0.022233076674090, 0.126357385491669, 0.851409537834241 } }, 0.015521786839045, 6 }, +} }; + +inline constexpr std::array rule_14 = { { + { { { 0.022072179275643, 0.488963910362179, 0.488963910362179 } }, 0.021883581369429, 3 }, + { { { 0.164710561319092, 0.417644719340454, 0.417644719340454 } }, 0.032788353544125, 3 }, + { { { 0.453044943382323, 0.273477528308839, 0.273477528308839 } }, 0.051774104507292, 3 }, + { { { 0.645588935174913, 0.177205532412543, 0.177205532412543 } }, 0.042162588736993, 3 }, + { { { 0.876400233818255, 0.061799883090873, 0.061799883090873 } }, 0.014433699669777, 3 }, + { { { 0.961218077502598, 0.019390961248701, 0.019390961248701 } }, 0.004923403602400, 3 }, + { { { 0.057124757403648, 0.172266687821356, 0.770608554774996 } }, 0.024665753212564, 6 }, + { { { 0.092916249356972, 0.336861459796345, 0.570222290846683 } }, 0.038571510787061, 6 }, + { { { 0.014646950055654, 0.298372882136258, 0.686980167808088 } }, 0.014436308113534, 6 }, + { { { 0.001268330932872, 0.118974497696957, 0.879757171370171 } }, 0.005010228838501, 6 }, +} }; + +inline constexpr std::array rule_15 = { { + { { { -0.013945833716486, 0.506972916858243, 0.506972916858243 } }, 0.001916875642849, 3 }, + { { { 0.137187291433955, 0.431406354283023, 0.431406354283023 } }, 0.044249027271145, 3 }, + { { { 0.444612710305711, 0.277693644847144, 0.277693644847144 } }, 0.051186548718852, 3 }, + { { { 0.747070217917492, 0.126464891041254, 0.126464891041254 } }, 0.023687735870688, 3 }, + { { { 0.858383228050628, 0.070808385974686, 0.070808385974686 } }, 0.013289775690021, 3 }, + { { { 0.962069659517853, 0.018965170241073, 0.018965170241073 } }, 0.004748916608192, 3 }, + { { { 0.133734161966621, 0.261311371140087, 0.604954466893291 } }, 0.038550072599593, 6 }, + { { { 0.036366677396917, 0.388046767090269, 0.575586555512814 } }, 0.027215814320624, 6 }, + { { { -0.010174883126571, 0.285712220049916, 0.724462663076655 } }, 0.002182077366797, 6 }, + { { { 0.036843869875878, 0.215599664072284, 0.747556466051838 } }, 0.021505319847731, 6 }, + { { { 0.012459809331199, 0.103575616576386, 0.883964574092416 } }, 0.007673942631049, 6 }, +} }; + +inline constexpr std::array rule_16 = { { + { { { 0.333333333333333, 0.333333333333333, 0.333333333333333 } }, 0.046875697427642, 1 }, + { { { 0.005238916103123, 0.497380541948438, 0.497380541948438 } }, 0.006405878578585, 3 }, + { { { 0.173061122901295, 0.413469438549352, 0.413469438549352 } }, 0.041710296739387, 3 }, + { { { 0.059082801866017, 0.470458599066991, 0.470458599066991 } }, 0.026891484250064, 3 }, + { { { 0.518892500060958, 0.240553749969521, 0.240553749969521 } }, 0.042132522761650, 3 }, + { { { 0.704068411554854, 0.147965794222573, 0.147965794222573 } }, 0.030000266842773, 3 }, + { { { 0.849069624685052, 0.075465187657474, 0.075465187657474 } }, 0.014200098925024, 3 }, + { { { 0.966807194753950, 0.016596402623025, 0.016596402623025 } }, 0.003582462351273, 3 }, + { { { 0.103575692245252, 0.296555596579887, 0.599868711174861 } }, 0.032773147460627, 6 }, + { { { 0.020083411655416, 0.337723063403079, 0.642193524941505 } }, 0.015298306248441, 6 }, + { { { -0.004341002614139, 0.204748281642812, 0.799592720971327 } }, 0.002386244192839, 6 }, + { { { 0.041941786468010, 0.189358492130623, 0.768699721401368 } }, 0.019084792755899, 6 }, + { { { 0.014317320230681, 0.085283615682657, 0.900399064086661 } }, 0.006850054546542, 6 }, +} }; + +inline constexpr std::array rule_17 = { { + { { { 0.333333333333333, 0.333333333333333, 0.333333333333333 } }, 0.033437199290803, 1 }, + { { { 0.005658918886452, 0.497170540556774, 0.497170540556774 } }, 0.005093415440507, 3 }, + { { { 0.035647354750751, 0.482176322624625, 0.482176322624625 } }, 0.014670864527638, 3 }, + { { { 0.099520061958437, 0.450239969020782, 0.450239969020782 } }, 0.024350878353672, 3 }, + { { { 0.199467521245206, 0.400266239377397, 0.400266239377397 } }, 0.031107550868969, 3 }, + { { { 0.495717464058095, 0.252141267970953, 0.252141267970953 } }, 0.031257111218620, 3 }, + { { { 0.675905990683077, 0.162047004658461, 0.162047004658461 } }, 0.024815654339665, 3 }, + { { { 0.848248235478508, 0.075875882260746, 0.075875882260746 } }, 0.014056073070557, 3 }, + { { { 0.968690546064356, 0.015654726967822, 0.015654726967822 } }, 0.003194676173779, 3 }, + { { { 0.010186928826919, 0.334319867363658, 0.655493203809423 } }, 0.008119655318993, 6 }, + { { { 0.135440871671036, 0.292221537796944, 0.572337590532020 } }, 0.026805742283163, 6 }, + { { { 0.054423924290583, 0.319574885423190, 0.626001190286228 } }, 0.018459993210822, 6 }, + { { { 0.012868560833637, 0.190704224192292, 0.796427214974071 } }, 0.008476868534328, 6 }, + { { { 0.067165782413524, 0.180483211648746, 0.752351005937729 } }, 0.018292796770025, 6 }, + { { { 0.014663182224828, 0.080711313679564, 0.904625504095608 } }, 0.006665632004165, 6 }, +} }; + +inline constexpr std::array rule_18 = { { + { { { 0.333333333333333, 0.333333333333333, 0.333333333333333 } }, 0.030809939937647, 1 }, + { { { 0.013310382738157, 0.493344808630921, 0.493344808630921 } }, 0.009072436679404, 3 }, + { { { 0.061578811516086, 0.469210594241957, 0.469210594241957 } }, 0.018761316939594, 3 }, + { { { 0.127437208225989, 0.436281395887006, 0.436281395887006 } }, 0.019441097985477, 3 }, + { { { 0.210307658653168, 0.394846170673416, 0.394846170673416 } }, 0.027753948610810, 3 }, + { { { 0.500410862393686, 0.249794568803157, 0.249794568803157 } }, 0.032256225351457, 3 }, + { { { 0.677135612512315, 0.161432193743843, 0.161432193743843 } }, 0.025074032616922, 3 }, + { { { 0.846803545029257, 0.076598227485371, 0.076598227485371 } }, 0.015271927971832, 3 }, + { { { 0.951495121293100, 0.024252439353450, 0.024252439353450 } }, 0.006793922022963, 3 }, + { { { 0.913707265566071, 0.043146367216965, 0.043146367216965 } }, -0.002223098729920, 3 }, + { { { 0.008430536202420, 0.358911494940944, 0.632657968856636 } }, 0.006331914076406, 6 }, + { { { 0.131186551737188, 0.294402476751957, 0.574410971510855 } }, 0.027257538049138, 6 }, + { { { 0.050203151565675, 0.325017801641814, 0.624779046792512 } }, 0.017676785649465, 6 }, + { { { 0.066329263810916, 0.184737559666046, 0.748933176523037 } }, 0.018379484638070, 6 }, + { { { 0.011996194566236, 0.218796800013321, 0.769207005420443 } }, 0.008104732808192, 6 }, + { { { 0.014858100590125, 0.101179597136408, 0.883962302273467 } }, 0.007634129070725, 6 }, + { { { -0.035222015287949, 0.020874755282586, 1.014347260005363 } }, 0.000046187660794, 6 }, +} }; + +inline constexpr std::array rule_19 = { { + { { { 0.333333333333333, 0.333333333333333, 0.333333333333333 } }, 0.032906331388919, 1 }, + { { { 0.020780025853987, 0.489609987073006, 0.489609987073006 } }, 0.010330731891272, 3 }, + { { { 0.090926214604215, 0.454536892697893, 0.454536892697893 } }, 0.022387247263016, 3 }, + { { { 0.197166638701138, 0.401416680649431, 0.401416680649431 } }, 0.030266125869468, 3 }, + { { { 0.488896691193805, 0.255551654403098, 0.255551654403098 } }, 0.030490967802198, 3 }, + { { { 0.645844115695741, 0.177077942152130, 0.177077942152130 } }, 0.024159212741641, 3 }, + { { { 0.779877893544096, 0.110061053227952, 0.110061053227952 } }, 0.016050803586801, 3 }, + { { { 0.888942751496321, 0.055528624251840, 0.055528624251840 } }, 0.008084580261784, 3 }, + { { { 0.974756272445543, 0.012621863777229, 0.012621863777229 } }, 0.002079362027485, 3 }, + { { { 0.003611417848412, 0.395754787356943, 0.600633794794645 } }, 0.003884876904981, 6 }, + { { { 0.134466754530780, 0.307929983880436, 0.557603261588784 } }, 0.025574160612022, 6 }, + { { { 0.014446025776115, 0.264566948406520, 0.720987025817365 } }, 0.008880903573338, 6 }, + { { { 0.046933578838178, 0.358539352205951, 0.594527068955871 } }, 0.016124546761731, 6 }, + { { { 0.002861120350567, 0.157807405968595, 0.839331473680839 } }, 0.002491941817491, 6 }, + { { { 0.223861424097916, 0.075050596975911, 0.701087978926173 } }, 0.018242840118951, 6 }, + { { { 0.034647074816760, 0.142421601113383, 0.822931324069857 } }, 0.010258563736199, 6 }, + { { { 0.010161119296278, 0.065494628082938, 0.924344252620784 } }, 0.003799928855302, 6 }, +} }; + +inline constexpr std::array rule_20 = { { + { { { 0.333333333333333, 0.333333333333333, 0.333333333333333 } }, 0.033057055541624, 1 }, + { { { -0.001900928704400, 0.500950464352200, 0.500950464352200 } }, 0.000867019185663, 3 }, + { { { 0.023574084130543, 0.488212957934729, 0.488212957934729 } }, 0.011660052716448, 3 }, + { { { 0.089726636099435, 0.455136681950283, 0.455136681950283 } }, 0.022876936356421, 3 }, + { { { 0.196007481363421, 0.401996259318289, 0.401996259318289 } }, 0.030448982673938, 3 }, + { { { 0.488214180481157, 0.255892909759421, 0.255892909759421 } }, 0.030624891725355, 3 }, + { { { 0.647023488009788, 0.176488255995106, 0.176488255995106 } }, 0.024368057676800, 3 }, + { { { 0.791658289326483, 0.104170855336758, 0.104170855336758 } }, 0.015997432032024, 3 }, + { { { 0.893862072318140, 0.053068963840930, 0.053068963840930 } }, 0.007698301815602, 3 }, + { { { 0.916762569607942, 0.041618715196029, 0.041618715196029 } }, -0.000632060497488, 3 }, + { { { 0.976836157186356, 0.011581921406822, 0.011581921406822 } }, 0.001751134301193, 3 }, + { { { 0.048741583664839, 0.344855770229001, 0.606402646106160 } }, 0.016465839189576, 6 }, + { { { 0.006314115948605, 0.377843269594854, 0.615842614456541 } }, 0.004839033540485, 6 }, + { { { 0.134316520547348, 0.306635479062357, 0.559048000390295 } }, 0.025804906534650, 6 }, + { { { 0.013973893962392, 0.249419362774742, 0.736606743262866 } }, 0.008471091054441, 6 }, + { { { 0.075549132909764, 0.212775724802802, 0.711675142287434 } }, 0.018354914106280, 6 }, + { { { -0.008368153208227, 0.146965436053239, 0.861402717154987 } }, 0.000704404677908, 6 }, + { { { 0.026686063258714, 0.137726978828923, 0.835586957912363 } }, 0.010112684927462, 6 }, + { { { 0.010547719294141, 0.059696109149007, 0.929756171556853 } }, 0.003573909385950, 6 }, +} }; + +/// Orbit table for Dunavant rule r in [1, 20]. +inline std::span +rule (int r) +{ + constexpr std::array, 20> table + = { rule_01, rule_02, rule_03, rule_04, rule_05, rule_06, rule_07, rule_08, rule_09, rule_10, + rule_11, rule_12, rule_13, rule_14, rule_15, rule_16, rule_17, rule_18, rule_19, rule_20 }; + return table[r - 1]; +} + +} // namespace t8_mra::dunavant_table + +#endif diff --git a/src/t8_mra/num/quadrature/gauss_legendre.hxx b/src/t8_mra/num/quadrature/gauss_legendre.hxx new file mode 100644 index 0000000000..dd785dbc2e --- /dev/null +++ b/src/t8_mra/num/quadrature/gauss_legendre.hxx @@ -0,0 +1,42 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include +#include + +#include + +namespace t8_mra +{ + +/** + * @brief Generates 1D Gauss-Legendre quadrature points and weights on [0,1]. + * + * The rule integrates polynomials up to degree 2*num_points-1 exactly. + * points and weights are overwritten with num_points entries. + */ +inline void +gauss_legendre_1d (int num_points, std::vector &points, std::vector &weights) +{ + if (num_points <= 0) + throw std::invalid_argument ("Number of quadrature points must be positive"); + + const std::unique_ptr workspace ( + gsl_integration_fixed_alloc (gsl_integration_fixed_legendre, num_points, 0.0, 1.0, 0.0, 0.0), + gsl_integration_fixed_free); + + if (!workspace) + throw std::runtime_error ("Failed to allocate GSL integration workspace"); + + const double *nodes = gsl_integration_fixed_nodes (workspace.get ()); + const double *w = gsl_integration_fixed_weights (workspace.get ()); + + points.assign (nodes, nodes + num_points); + weights.assign (w, w + num_points); +} + +} // namespace t8_mra + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/num/quadrature/quadrature.hxx b/src/t8_mra/num/quadrature/quadrature.hxx new file mode 100644 index 0000000000..79e57fba24 --- /dev/null +++ b/src/t8_mra/num/quadrature/quadrature.hxx @@ -0,0 +1,127 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include +#include +#include +#include +#include + +#include + +#include "t8_mra/core/shape_traits.hxx" +#include "t8_mra/num/quadrature/gauss_legendre.hxx" +#include "t8_mra/num/quadrature/dunavant.hxx" + +namespace t8_mra +{ + +// ============================================================================ +// Reference-element quadrature rule +// ============================================================================ +// One specialization of quadrature per shape provides the integration +// points and weights on the reference element, behind a common interface. +// Cartesian shapes use a tensor product of 1D Gauss-Legendre; the triangle +// uses a Dunavant rule. Mirrors basis (num/basis/basis.hxx). +// +// Adding a new shape: specialize quadrature with points/weights and +// a constructor taking the rule's accuracy parameter. + +/// Common interface every quadrature specialization provides: a flat list of +/// num_points reference points (DIM coords each, point q at points[DIM*q + d]) +/// and matching weights. +template +concept quadrature_rule = requires (const TQuadrature q) { + { TQuadrature::DIM } -> std::convertible_to; + { q.num_points } -> std::convertible_to; + { q.points.data () } -> std::convertible_to; + { q.weights.data () } -> std::convertible_to; +}; + +template +struct quadrature; + +/// Cartesian shapes: tensor product of a 1D Gauss-Legendre rule, exact to +/// degree 2*num_points_1d - 1 per axis. +template +struct quadrature>> +{ + static constexpr int DIM = shape_traits::DIM; + + std::size_t num_points = 0; + std::vector points; // flattened: point q coord d at points[DIM*q + d] + std::vector weights; + + /// 1D point count for a rule exact to the given polynomial degree (2n-1 >= degree). + [[nodiscard]] static constexpr int + rule_for_degree (int degree) + { + return degree / 2 + 1; + } + + quadrature () = default; + + explicit quadrature (int num_points_1d) + { + std::vector p1d; + std::vector w1d; + gauss_legendre_1d (num_points_1d, p1d, w1d); + + num_points = 1; + for (auto d = 0; d < DIM; ++d) + num_points *= num_points_1d; + + points.resize (DIM * num_points); + weights.resize (num_points); + + // Odometer over the DIM axes (first axis fastest); order is irrelevant to + // the integration sum, so any consistent enumeration works. + for (auto q = 0; q < num_points; ++q) { + auto rest = q; + double w = 1.0; + + for (auto d = 0; d < DIM; ++d) { + const auto id = rest % num_points_1d; + rest /= num_points_1d; + points[DIM * q + d] = p1d[id]; + w *= w1d[id]; + } + + weights[q] = w; + } + } +}; + +/// Triangle: a Dunavant rule on the reference triangle. +template <> +struct quadrature +{ + static constexpr int DIM = 2; + + std::size_t num_points = 0; + std::vector points; // flattened: [x0, y0, x1, y1, ...] + std::vector weights; + + /// Dunavant rule (accuracy degree) for the given polynomial degree, capped at the table maximum. + [[nodiscard]] static constexpr int + rule_for_degree (int degree) + { + return std::min (20, degree); + } + + quadrature () = default; + + explicit quadrature (int rule) + { + auto rule_data = dunavant_rule (rule); + num_points = rule_data.weights.size (); + points = std::move (rule_data.points); + weights = std::move (rule_data.weights); + } +}; + +} // namespace t8_mra + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/num/shape/cartesian.hxx b/src/t8_mra/num/shape/cartesian.hxx new file mode 100644 index 0000000000..02e4397e65 --- /dev/null +++ b/src/t8_mra/num/shape/cartesian.hxx @@ -0,0 +1,220 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include +#include + +#include "t8_mra/core/shape_traits.hxx" +#include "t8_mra/num/basis/basis.hxx" +#include "t8_mra/num/basis/legendre.hxx" +#include "t8_mra/num/cell_geometry.hxx" + +namespace t8_mra +{ + +/// Cartesian shapes (LINE, QUAD, HEX): tensor product of 1D Legendre modes, the +/// basis index decomposed lexicographically (first coordinate fastest). +/// Orthonormal on the reference cell, so no volume normalization. +template + requires is_cartesian +struct basis +{ + static constexpr int DIM = shape_traits::DIM; + static constexpr int DOF = shape_traits::dof (P); + + [[nodiscard]] static std::array + eval (const std::array &x) + { + std::array res = {}; + for (int p = 0; p < DOF; ++p) { + double v = 1.0; + int idx = p; + for (int d = 0; d < DIM; ++d) { + v *= phi_1d (x[d], idx % P); + idx /= P; + } + res[p] = v; + } + return res; + } + + [[nodiscard]] static std::array, DIM> + eval_gradient (const std::array &x) + { + std::array, DIM> grad = {}; + for (int dir = 0; dir < DIM; ++dir) { + for (int p = 0; p < DOF; ++p) { + double v = 1.0; + int idx = p; + for (int d = 0; d < DIM; ++d) { + const int deg = idx % P; + idx /= P; + v *= (d == dir) ? phi_prime_1d

(x[d], deg) : phi_1d (x[d], deg); + } + grad[dir][p] = v; + } + } + return grad; + } + + [[nodiscard]] static constexpr double + normalization (double) noexcept + { + return 1.0; + } +}; + +/** @brief Cartesian leaf geometry: axis-aligned box, diagonal Jacobian. */ +template + requires is_cartesian +struct cell_geometry +{ + static constexpr int DIM = shape_traits::DIM; + static constexpr int DOF = shape_traits::dof (P); + using basis_t = basis; + using point = std::array; + + point origin {}; + point extent {}; + double volume = 0.0; + double basis_scale = 1.0; + double mass = 0.0; + int level = 0; + + /** @brief Build from the cell's min/max corners. */ + [[nodiscard]] static cell_geometry + from_box (const point &min_corner, const point &max_corner, double vol) + { + cell_geometry geom; + geom.origin = min_corner; + + double det = 1.0; + for (int d = 0; d < DIM; ++d) { + geom.extent[d] = max_corner[d] - min_corner[d]; + det *= geom.extent[d]; + } + + geom.volume = vol; + geom.basis_scale = basis_t::normalization (vol); + geom.mass = geom.basis_scale * geom.basis_scale * std::abs (det); + + return geom; + } + + /** @brief Reference coordinate -> basis coordinate (identity). */ + [[nodiscard]] static point + basis_coord (const point &ref) + { + return ref; + } + + /** @brief Pin a shared-face point's normal reference component exactly (t8 face: axis=f>>1, side=f&1). */ + [[nodiscard]] static point + on_face (point ref, int face) + { + ref[face >> 1] = (face & 1) ? 1.0 : 0.0; + return ref; + } + + /** @brief Whether a reference point lies in the unit box. */ + [[nodiscard]] static bool + in_ref_cell (const point &ref) + { + for (int d = 0; d < DIM; ++d) + if (ref[d] < -reference_cell_tol || ref[d] > 1.0 + reference_cell_tol) + return false; + + return true; + } + + /** @brief Physical -> reference coordinate. */ + [[nodiscard]] point + to_reference (const point &phys) const + { + point ref {}; + for (int d = 0; d < DIM; ++d) + ref[d] = (phys[d] - origin[d]) / extent[d]; + + return ref; + } + + /** @brief Reference -> physical coordinate. */ + [[nodiscard]] point + to_physical (const point &ref) const + { + point phys {}; + for (int d = 0; d < DIM; ++d) + phys[d] = origin[d] + extent[d] * ref[d]; + + return phys; + } + + /** @brief Whether a physical point lies in the cell. */ + [[nodiscard]] bool + contains (const point &phys) const + { + return in_ref_cell (to_reference (phys)); + } + + /** @brief basis_scale * sum_i coeffs_i * phi_i(basis_coord(ref)). */ + [[nodiscard]] static double + eval_modal (std::span coeffs, const point &ref, double basis_scale) + { + const auto phi = basis_t::eval (basis_coord (ref)); + double sum = 0.0; + for (int i = 0; i < DOF; ++i) + sum += coeffs[i] * phi[i]; + + return basis_scale * sum; + } + + /** @brief Physical value at a reference point from the cell volume alone (no cell map). */ + [[nodiscard]] static double + reference_value (std::span coeffs, const point &ref, double volume) + { + return eval_modal (coeffs, ref, basis_t::normalization (volume)); + } + + /** @brief Physical value at a reference point using the cached basis scale. */ + [[nodiscard]] double + value (std::span coeffs, const point &ref) const + { + return eval_modal (coeffs, ref, basis_scale); + } + + /** @brief Physical gradient d(u_h)/d(x_d) at a reference point. */ + [[nodiscard]] point + gradient (std::span coeffs, const point &ref) const + { + const auto ref_grad = basis_t::eval_gradient (ref); + point grad {}; + + for (int d = 0; d < DIM; ++d) { + double sum = 0.0; + for (int i = 0; i < DOF; ++i) + sum += coeffs[i] * ref_grad[d][i]; + + grad[d] = basis_scale * sum / extent[d]; + } + + return grad; + } + + /** @brief inv_jac * phys_dir: weights w with (phys_dir . grad_x phi) = w . grad_r phi. */ + [[nodiscard]] point + reference_direction (const point &phys_dir) const + { + point ref_dir {}; + + for (int d = 0; d < DIM; ++d) + ref_dir[d] = phys_dir[d] / extent[d]; + + return ref_dir; + } +}; + +} // namespace t8_mra + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/num/shape/triangle.hxx b/src/t8_mra/num/shape/triangle.hxx new file mode 100644 index 0000000000..ddd0cb4e21 --- /dev/null +++ b/src/t8_mra/num/shape/triangle.hxx @@ -0,0 +1,202 @@ +#pragma once + +#ifdef T8_ENABLE_MRA + +#include +#include +#include +#include + +#include "t8_mra/core/shape_traits.hxx" +#include "t8_mra/num/basis/basis.hxx" +#include "t8_mra/num/basis/dubiner.hxx" +#include "t8_mra/num/cell_geometry.hxx" + +namespace t8_mra +{ + +/// Triangle: the orthonormal Dubiner basis in barycentric coords +/// (x = {lambda0, lambda1}). Orthonormal on the reference triangle (area 1/2), +/// so the physical basis scales by sqrt(1/(2*vol)). +template +struct basis +{ + static constexpr int DIM = 2; + static constexpr int DOF = shape_traits::dof (P); + + [[nodiscard]] static std::array + eval (const std::array &x) + { + return [&] (std::index_sequence) { + return std::array { scaling_function (I)> (x[0], x[1])... }; + }(std::make_index_sequence {}); + } + + /// grad[dir][i] = d(phi_i)/dx_dir on the reference triangle (the geometric + /// Jacobian to physical coordinates is applied by the caller). + [[nodiscard]] static std::array, DIM> + eval_gradient (const std::array &x) + { + std::array, DIM> grad = {}; + [&] (std::index_sequence) { + ( + [&] { + const auto g = scaling_function_gradient (I)> (x[0], x[1]); + grad[0][I] = g[0]; + grad[1][I] = g[1]; + }(), + ...); + }(std::make_index_sequence {}); + return grad; + } + + [[nodiscard]] static double + normalization (double vol) + { + return std::sqrt (1.0 / (2.0 * vol)); + } +}; + +/** @brief Triangle leaf geometry: general affine map from three ordered vertices. */ +template +struct cell_geometry +{ + static constexpr int DIM = 2; + static constexpr int DOF = shape_traits::dof (P); + using basis_t = basis; + using point = std::array; + + point origin {}; + std::array edges {}; // x_d = origin_d + sum_e edges[d][e] * ref_e + std::array inv_jac {}; // ref_e = sum_d inv_jac[e][d] * (x_d - origin_d) + double volume = 0.0; + double basis_scale = 0.0; + double mass = 0.0; + int level = 0; + + /** @brief Build from the ordered vertices (origin, r0 edge, r1 edge). */ + [[nodiscard]] static cell_geometry + from_triangle (const point &v0, const point &v1, const point &v2, double vol) + { + cell_geometry geom; + geom.origin = v0; + geom.edges = { point { v1[0] - v0[0], v2[0] - v0[0] }, point { v1[1] - v0[1], v2[1] - v0[1] } }; + + const double J00 = geom.edges[0][0], J01 = geom.edges[0][1], J10 = geom.edges[1][0], J11 = geom.edges[1][1]; + const double det = J00 * J11 - J01 * J10; + geom.inv_jac = { point { J11 / det, -J01 / det }, point { -J10 / det, J00 / det } }; + + geom.volume = vol; + geom.basis_scale = basis_t::normalization (vol); + geom.mass = geom.basis_scale * geom.basis_scale * std::abs (det); + + return geom; + } + + /** @brief Reference (r0, r1) -> Dubiner coordinate {lambda0, lambda1}. */ + [[nodiscard]] static point + basis_coord (const point &ref) + { + return { 1.0 - ref[0] - ref[1], ref[0] }; + } + + /** @brief Whether a reference point lies in the unit triangle. */ + [[nodiscard]] static bool + in_ref_cell (const point &ref) + { + return ref[0] >= -reference_cell_tol && ref[1] >= -reference_cell_tol + && ref[0] + ref[1] <= 1.0 + reference_cell_tol; + } + + /** @brief Physical -> reference coordinate. */ + [[nodiscard]] point + to_reference (const point &phys) const + { + const double dx = phys[0] - origin[0], dy = phys[1] - origin[1]; + + return { inv_jac[0][0] * dx + inv_jac[0][1] * dy, inv_jac[1][0] * dx + inv_jac[1][1] * dy }; + } + + /** @brief Reference -> physical coordinate. */ + [[nodiscard]] point + to_physical (const point &ref) const + { + return { origin[0] + edges[0][0] * ref[0] + edges[0][1] * ref[1], + origin[1] + edges[1][0] * ref[0] + edges[1][1] * ref[1] }; + } + + /** @brief Whether a physical point lies in the cell. */ + [[nodiscard]] bool + contains (const point &phys) const + { + return in_ref_cell (to_reference (phys)); + } + + /** @brief basis_scale * sum_i coeffs_i * phi_i(basis_coord(ref)). */ + [[nodiscard]] static double + eval_modal (std::span coeffs, const point &ref, double basis_scale) + { + const auto phi = basis_t::eval (basis_coord (ref)); + double sum = 0.0; + for (int i = 0; i < DOF; ++i) + sum += coeffs[i] * phi[i]; + + return basis_scale * sum; + } + + /** @brief Physical value at a reference point from the cell volume alone (no cell map). */ + [[nodiscard]] static double + reference_value (std::span coeffs, const point &ref, double volume) + { + return eval_modal (coeffs, ref, basis_t::normalization (volume)); + } + + /** @brief Physical value at a reference point using the cached basis scale. */ + [[nodiscard]] double + value (std::span coeffs, const point &ref) const + { + return eval_modal (coeffs, ref, basis_scale); + } + + /** @brief Physical gradient d(u_h)/d(x_d) at a reference point. */ + [[nodiscard]] point + gradient (std::span coeffs, const point &ref) const + { + const auto ref_grad = to_ref_grad (basis_t::eval_gradient (basis_coord (ref))); + point grad {}; + for (int d = 0; d < DIM; ++d) { + double sum = 0.0; + for (int i = 0; i < DOF; ++i) + sum += coeffs[i] * (ref_grad[0][i] * inv_jac[0][d] + ref_grad[1][i] * inv_jac[1][d]); + grad[d] = basis_scale * sum; + } + + return grad; + } + + /** @brief inv_jac * phys_dir: weights w with (phys_dir . grad_x phi) = w . grad_r phi. */ + [[nodiscard]] point + reference_direction (const point &phys_dir) const + { + return { inv_jac[0][0] * phys_dir[0] + inv_jac[0][1] * phys_dir[1], + inv_jac[1][0] * phys_dir[0] + inv_jac[1][1] * phys_dir[1] }; + } + + private: + /** @brief Basis gradient d/dlambda -> reference d/dr (r0=lambda1, r1=lambda2). */ + [[nodiscard]] static std::array, 2> + to_ref_grad (const std::array, 2> &basis_grad) + { + std::array, 2> ref_grad {}; + for (int i = 0; i < DOF; ++i) { + ref_grad[0][i] = basis_grad[1][i] - basis_grad[0][i]; + ref_grad[1][i] = -basis_grad[0][i]; + } + + return ref_grad; + } +}; + +} // namespace t8_mra + +#endif // T8_ENABLE_MRA diff --git a/src/t8_mra/t8_mra.hxx b/src/t8_mra/t8_mra.hxx new file mode 100644 index 0000000000..d1392ed6e7 --- /dev/null +++ b/src/t8_mra/t8_mra.hxx @@ -0,0 +1,57 @@ +#pragma once + +/** \file t8_mra.hxx + * Main header of the t8code multiresolution analysis (MRA) module. + * + * The module maintains element-local DG data together with an adaptive + * forest. A multiscale transformation decomposes the data into coarse + * approximations and detail coefficients; adaptation criteria act on those + * details: coarsening removes cells that carry no essential information, + * refinement adds resolution where a criterion demands it. The criteria are + * exchangeable (see criteria/), defaults implement Harten-style hard + * thresholding and prediction. + * + * Central class: t8_mra::multiscale with element shape TShape, + * U solution components and polynomial order P. + * + * Basic usage: + * \code + * #include + * + * t8_mra::multiscale mra (max_level, comm); + * + * // Project a function onto a uniform forest of level max_level ... + * mra.initialize_data (cmesh, scheme, max_level, func); + * + * // ... or build the grid bottom-up: refine towards max_level only where + * // the coarsening criterion finds significant details (never builds the + * // uniform fine grid) + * mra.initialize_data_adaptive (cmesh, scheme, max_level, func); + * + * // Adapt: defaults are hard thresholding / Harten's prediction ... + * mra.coarsen (min_level, max_level); + * mra.refine (min_level, max_level); + * + * // ... with adjustable parameters, or any type satisfying the + * // coarsening/refinement_criterion concept + * mra.coarsen (min_level, max_level, t8_mra::hard_thresholding { .c_thresh = 0.1 }); + * mra.refine (min_level, max_level, my_criterion {}); + * + * t8_mra::write_forest_lagrange_vtk (mra, "solution", P - 1); + * + * mra.cleanup (); + * \endcode + */ + +#ifdef T8_ENABLE_MRA + +#include +#include +#include +#include +#include +#include +#include +#include + +#endif // T8_ENABLE_MRA diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ee0f214509..9685669ae3 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -225,6 +225,29 @@ endif() add_t8_cpp_test( NAME t8_gtest_vector_split_serial SOURCES t8_helper_functions/t8_gtest_vector_split.cxx ) +if( T8CODE_ENABLE_MRA ) + add_t8_cpp_test( NAME t8_gtest_mra_adaptation_serial SOURCES t8_mra/t8_gtest_mra_adaptation.cxx ) + add_t8_cpp_test( NAME t8_gtest_mra_data_serial SOURCES t8_mra/t8_gtest_mra_data.cxx ) + add_t8_cpp_test( NAME t8_gtest_mra_num_serial SOURCES t8_mra/t8_gtest_mra_num.cxx ) + add_t8_cpp_test( NAME t8_gtest_mra_cell_geometry_serial SOURCES t8_mra/t8_gtest_mra_cell_geometry.cxx ) + add_t8_cpp_test( NAME t8_gtest_mra_mst_serial SOURCES t8_mra/t8_gtest_mra_mst.cxx ) + add_t8_cpp_test( NAME t8_gtest_mra_criteria_serial SOURCES t8_mra/t8_gtest_mra_criteria.cxx ) + add_t8_cpp_test( NAME t8_gtest_mra_projection_serial SOURCES t8_mra/t8_gtest_mra_projection.cxx ) + add_t8_cpp_test( NAME t8_gtest_mra_mpi_serial SOURCES t8_mra/t8_gtest_mra_mpi.cxx ) + add_t8_cpp_test( NAME t8_gtest_mra_mpi_parallel SOURCES t8_mra/t8_gtest_mra_mpi.cxx ) + set_tests_properties( t8_gtest_mra_mpi_serial PROPERTIES FIXTURES_SETUP mra_mpi_ref ) + set_tests_properties( t8_gtest_mra_mpi_parallel PROPERTIES FIXTURES_REQUIRED mra_mpi_ref ) + # Partition-independence must hold at non-power-of-2 rank counts too; reuse the + # parallel binary and the same reference the serial run writes. + if( T8CODE_ENABLE_MPI ) + foreach( np 2 3 ) + add_test( NAME t8_gtest_mra_mpi_np${np} + COMMAND mpirun -np ${np} $ ) + set_tests_properties( t8_gtest_mra_mpi_np${np} PROPERTIES FIXTURES_REQUIRED mra_mpi_ref ) + endforeach() + endif() +endif() + if( T8CODE_BUILD_MESH_HANDLE ) add_t8_cpp_test( NAME t8_gtest_mesh_handle_parallel SOURCES mesh_handle/t8_gtest_mesh_handle.cxx ) add_t8_cpp_test( NAME t8_gtest_compare_handle_to_forest_serial SOURCES mesh_handle/t8_gtest_compare_handle_to_forest.cxx ) diff --git a/test/t8_mra/t8_gtest_mra_adaptation.cxx b/test/t8_mra/t8_gtest_mra_adaptation.cxx new file mode 100644 index 0000000000..ec2884b005 --- /dev/null +++ b/test/t8_mra/t8_gtest_mra_adaptation.cxx @@ -0,0 +1,422 @@ +/* + This file is part of t8code. + t8code is a C library to manage a collection (a forest) of multiple + connected adaptive space-trees of general element classes in parallel. + + Copyright (C) 2026 the developers + + t8code is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + t8code is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with t8code; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +/** \file t8_gtest_mra_adaptation.cxx + * Regression tests for the MRA path (multiscale_base + mst + + * multiscale_adaptation), typed over (element shape, U components, order P): + * - forward/inverse multiscale transformation round-trip is the identity + * - coarsen -> refine -> coarsen returns to the coarsened grid and data + * - lmi_map mirrors the forest leaves exactly after every adaptation step + * + * Shapes: TRIANGLE (hardcoded masks, P = 1..4), QUAD and HEX (computed + * masks, arbitrary P). + */ + +#include + +#ifdef T8_ENABLE_MRA + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace +{ + +constexpr double eps = 1e-12; + +template +struct Config +{ + static constexpr t8_eclass Shape = TShape; + static constexpr int U = U_; + static constexpr int P = P_; + static constexpr int DIM = t8_mra::shape_traits::DIM; +}; + +/* Smooth test function for the MST round-trip; components differ to catch + * component-indexing bugs. */ +template +auto +smooth_func () +{ + if constexpr (DIM == 2) + return [] (double x, double y) { + std::array res; + for (auto u = 0; u < U; ++u) + res[u] = std::sin (2.0 * M_PI * (u + 1) * x) * std::sin (2.0 * M_PI * y); + return res; + }; + else + return [] (double x, double y, double z) { + std::array res; + for (auto u = 0; u < U; ++u) + res[u] = std::sin (2.0 * M_PI * (u + 1) * x) * std::sin (2.0 * M_PI * y) * std::sin (2.0 * M_PI * z); + return res; + }; +} + +/* Discontinuous test function: jump along a circle/sphere segment. Each side is + * a polynomial of total degree P-1, hence exactly representable at order P: the + * smooth regions carry zero detail and coarsen down to the base, while the two + * sides differ across the interface and keep a refined band there. Built per P + * so the coarsen/refine round-trip is a genuine invariant for every order, + * including P = 1 (piecewise constant). */ +template +auto +jump_func () +{ + constexpr int d = P - 1; + if constexpr (DIM == 2) + return [] (double x, double y) { + std::array res; + const double r = x * x + y * y; + const double in = std::pow (0.5 + x + y, d); + const double out = std::pow (0.3 + x - y, d); + for (auto u = 0; u < U; ++u) + res[u] = (u + 1) * ((r < 0.25) ? (3.0 + in) : out); + return res; + }; + else + return [] (double x, double y, double z) { + std::array res; + const double r = x * x + y * y + z * z; + const double in = std::pow (0.5 + x + y + z, d); + const double out = std::pow (0.3 + x - y + z, d); + for (auto u = 0; u < U; ++u) + res[u] = (u + 1) * ((r < 0.25) ? (3.0 + in) : out); + return res; + }; +} + +template +t8_mra::multiscale +make_mra (int max_level) +{ + return t8_mra::multiscale (max_level, sc_MPI_COMM_WORLD); +} + +/* Every forest leaf must have its lmi stored in lmi_idx with the element's + * level, and exactly the leaves must be present in lmi_map. */ +template +void +expect_forest_map_consistent (MRA &mra) +{ + auto *forest = mra.get_forest (); + auto *user_data = mra.get_user_data (); + auto *lmi_map = mra.get_lmi_map (); + const auto *scheme = t8_forest_get_scheme (forest); + + ASSERT_EQ (static_cast (t8_forest_get_local_num_leaf_elements (forest)), lmi_map->size ()); + + auto current_idx = t8_locidx_t { 0 }; + const auto num_trees = t8_forest_get_num_local_trees (forest); + for (t8_locidx_t tree_idx = 0; tree_idx < num_trees; ++tree_idx) { + const auto tree_class = t8_forest_get_tree_class (forest, tree_idx); + const auto num_elements = t8_forest_get_tree_num_leaf_elements (forest, tree_idx); + + for (t8_locidx_t ele_idx = 0; ele_idx < num_elements; ++ele_idx, ++current_idx) { + const auto *element = t8_forest_get_leaf_element_in_tree (forest, tree_idx, ele_idx); + const auto lmi = t8_mra::get_lmi_from_forest_data (user_data, current_idx); + + EXPECT_EQ (lmi.level (), static_cast (scheme->element_get_level (tree_class, element))); + EXPECT_TRUE (lmi_map->contains (lmi)); + } + } +} + +template +void +expect_maps_equal (const MapT &expected, const MapT &actual, unsigned int max_level, double tol) +{ + for (auto l = 0u; l <= max_level; ++l) { + ASSERT_EQ (expected[l].size (), actual[l].size ()) << "entry count differs on level " << l; + + for (const auto &[lmi, data] : expected[l]) { + ASSERT_TRUE (actual.contains (lmi)) << "missing lmi on level " << l; + + const auto &other = actual.get (lmi); + ASSERT_EQ (data.u_coeffs.size (), other.u_coeffs.size ()); + for (auto i = 0u; i < data.u_coeffs.size (); ++i) + EXPECT_NEAR (data.u_coeffs[i], other.u_coeffs[i], tol) << "coeff " << i << " on level " << l; + } + } +} + +/* Constant initial data: all detail coefficients vanish exactly, so the + * bottom-up initialization must never keep a refined level. */ +template +auto +constant_func () +{ + if constexpr (DIM == 2) + return [] (double, double) { + std::array res; + for (auto u = 0; u < U; ++u) + res[u] = 3.0 * (u + 1); + return res; + }; + else + return [] (double, double, double) { + std::array res; + for (auto u = 0; u < U; ++u) + res[u] = 3.0 * (u + 1); + return res; + }; +} + +/* Custom coarsening criterion without prepare(): nothing is significant + * -> coarsening must collapse the grid completely. */ +struct collapse_criterion +{ + template + bool + significant (MRA &, const typename MRA::levelmultiindex &) + { + return false; + } +}; + +template +class mra_adaptation: public ::testing::Test {}; + +using Configs = ::testing::Types< + /* Triangle: hardcoded masks, P = 1..4 */ + Config, Config, Config, + Config, Config, + /* Quad: computed masks */ + Config, Config, Config, + Config, Config, + /* Hex (3D): computed masks */ + Config, Config, Config>; + +struct ConfigNames +{ + template + static std::string + GetName (int) + { + return std::string (t8_eclass_to_string[T::Shape]) + "_U" + std::to_string (T::U) + "_P" + std::to_string (T::P); + } +}; + +TYPED_TEST_SUITE (mra_adaptation, Configs, ConfigNames); + +/* Forward then inverse MST over the full level range must reproduce the + * single-scale data up to round-off. */ +TYPED_TEST (mra_adaptation, mst_roundtrip) +{ + constexpr auto Shape = TypeParam::Shape; + constexpr auto U = TypeParam::U; + constexpr auto P = TypeParam::P; + constexpr auto DIM = TypeParam::DIM; + + const int max_level = (DIM == 3) ? 3 : 4; + auto mra = make_mra (max_level); + + t8_cmesh_t cmesh = t8_cmesh_new_hypercube (Shape, sc_MPI_COMM_WORLD, 0, 0, 0); + auto *scheme = t8_scheme_new_default (); + /* The forest takes ownership of cmesh and scheme; keep our own references */ + t8_cmesh_ref (cmesh); + t8_scheme_ref (const_cast (scheme)); + + mra.initialize_data (cmesh, scheme, max_level, smooth_func ()); + expect_forest_map_consistent (mra); + + const auto snapshot = *mra.get_lmi_map (); + + mra.multiscale_decomposition (0, max_level); + /* All single-scale data restricted to level 0 */ + EXPECT_EQ (mra.get_lmi_map ()->size (), mra.get_lmi_map ()->size (0)); + + mra.inverse_multiscale_transformation (0, max_level); + expect_maps_equal (snapshot, *mra.get_lmi_map (), max_level, eps); + + mra.cleanup (); + t8_cmesh_destroy (&cmesh); + t8_scheme_unref (const_cast (&scheme)); +} + +/* Coarsening discards only non-significant details; refinement adds only + * zero-detail children (plus Harten grading). A second coarsening therefore + * must return exactly to the first coarsened grid and data. */ +TYPED_TEST (mra_adaptation, coarsen_refine_roundtrip) +{ + constexpr auto Shape = TypeParam::Shape; + constexpr auto U = TypeParam::U; + constexpr auto P = TypeParam::P; + constexpr auto DIM = TypeParam::DIM; + + const int max_level = (DIM == 3) ? 4 : 5; + auto mra = make_mra (max_level); + + t8_cmesh_t cmesh = t8_cmesh_new_hypercube (Shape, sc_MPI_COMM_WORLD, 0, 0, 0); + auto *scheme = t8_scheme_new_default (); + t8_cmesh_ref (cmesh); + t8_scheme_ref (const_cast (scheme)); + + mra.initialize_data (cmesh, scheme, max_level, jump_func ()); + const auto num_uniform = t8_forest_get_global_num_leaf_elements (mra.get_forest ()); + + mra.coarsen (0, max_level); + expect_forest_map_consistent (mra); + const auto num_coarse = t8_forest_get_global_num_leaf_elements (mra.get_forest ()); + EXPECT_LT (num_coarse, num_uniform) << "smooth regions must coarsen"; + + const auto snapshot = *mra.get_lmi_map (); + + mra.refine (0, max_level); + expect_forest_map_consistent (mra); + const auto num_refined = t8_forest_get_global_num_leaf_elements (mra.get_forest ()); + EXPECT_GT (num_refined, num_coarse) << "neighbour prediction must refine around the jump"; + + mra.coarsen (0, max_level); + expect_forest_map_consistent (mra); + const auto num_recoarse = t8_forest_get_global_num_leaf_elements (mra.get_forest ()); + EXPECT_EQ (num_recoarse, num_coarse) << "zero-detail children must coarsen away again"; + + expect_maps_equal (snapshot, *mra.get_lmi_map (), max_level, eps); + + mra.cleanup (); + t8_cmesh_destroy (&cmesh); + t8_scheme_unref (const_cast (&scheme)); +} + +/* A criterion that finds nothing significant must collapse the grid to the + * base cells, regardless of the data. */ +TYPED_TEST (mra_adaptation, custom_criterion_collapse) +{ + constexpr auto Shape = TypeParam::Shape; + constexpr auto U = TypeParam::U; + constexpr auto P = TypeParam::P; + constexpr auto DIM = TypeParam::DIM; + + const int max_level = (DIM == 3) ? 3 : 4; + auto mra = make_mra (max_level); + + t8_cmesh_t cmesh = t8_cmesh_new_hypercube (Shape, sc_MPI_COMM_WORLD, 0, 0, 0); + auto *scheme = t8_scheme_new_default (); + t8_cmesh_ref (cmesh); + t8_scheme_ref (const_cast (scheme)); + + mra.initialize_data (cmesh, scheme, max_level, jump_func ()); + + mra.coarsen (0, max_level, collapse_criterion {}); + expect_forest_map_consistent (mra); + + EXPECT_EQ (t8_forest_get_global_num_leaf_elements (mra.get_forest ()), + t8_forest_get_num_global_trees (mra.get_forest ())); + + mra.cleanup (); + t8_cmesh_destroy (&cmesh); + t8_scheme_unref (const_cast (&scheme)); +} + +/* Bottom-up initialization on constant data: every detail is exactly zero, + * so nothing is significant and the result must be the level-1 grid with + * the level-1 projection. */ +TYPED_TEST (mra_adaptation, bottom_up_constant_collapses) +{ + constexpr auto Shape = TypeParam::Shape; + constexpr auto U = TypeParam::U; + constexpr auto P = TypeParam::P; + constexpr auto DIM = TypeParam::DIM; + + const int max_level = (DIM == 3) ? 3 : 4; + auto mra = make_mra (max_level); + auto reference = make_mra (max_level); + + t8_cmesh_t cmesh = t8_cmesh_new_hypercube (Shape, sc_MPI_COMM_WORLD, 0, 0, 0); + auto *scheme = t8_scheme_new_default (); + /* Two forests consume one reference each; keep our own on top */ + t8_cmesh_ref (cmesh); + t8_cmesh_ref (cmesh); + t8_scheme_ref (const_cast (scheme)); + t8_scheme_ref (const_cast (scheme)); + + mra.initialize_data_adaptive (cmesh, scheme, max_level, constant_func ()); + expect_forest_map_consistent (mra); + + reference.initialize_data (cmesh, scheme, 1, constant_func ()); + + EXPECT_EQ (t8_forest_get_global_num_leaf_elements (mra.get_forest ()), + t8_forest_get_global_num_leaf_elements (reference.get_forest ())); + expect_maps_equal (*reference.get_lmi_map (), *mra.get_lmi_map (), max_level, eps); + + mra.cleanup (); + reference.cleanup (); + t8_cmesh_destroy (&cmesh); + t8_scheme_unref (const_cast (&scheme)); +} + +/* Bottom-up initialization on discontinuous data must produce an adaptive + * grid: strictly coarser than uniform, refined to max_level at the jump, + * and a valid starting point for the regular adaptation cycle. */ +TYPED_TEST (mra_adaptation, bottom_up_adaptive_grid) +{ + constexpr auto Shape = TypeParam::Shape; + constexpr auto U = TypeParam::U; + constexpr auto P = TypeParam::P; + constexpr auto DIM = TypeParam::DIM; + + using levelmultiindex = typename t8_mra::multiscale::levelmultiindex; + + const int max_level = (DIM == 3) ? 4 : 5; + auto mra = make_mra (max_level); + + t8_cmesh_t cmesh = t8_cmesh_new_hypercube (Shape, sc_MPI_COMM_WORLD, 0, 0, 0); + auto *scheme = t8_scheme_new_default (); + t8_cmesh_ref (cmesh); + t8_scheme_ref (const_cast (scheme)); + + mra.initialize_data_adaptive (cmesh, scheme, max_level, jump_func ()); + expect_forest_map_consistent (mra); + + const auto num_adaptive = t8_forest_get_global_num_leaf_elements (mra.get_forest ()); + const auto num_trees = t8_forest_get_num_global_trees (mra.get_forest ()); + const auto num_uniform = num_trees * static_cast (std::pow (levelmultiindex::NUM_CHILDREN, max_level)); + + EXPECT_LT (num_adaptive, num_uniform) << "smooth regions must stay coarse"; + EXPECT_GT (mra.get_lmi_map ()->size (max_level), 0u) << "the jump must reach max_level"; + EXPECT_EQ (mra.get_lmi_map ()->size (0), 0u) << "level 1 is the minimum level"; + + /* The result must be a valid input for the regular adaptation cycle */ + mra.coarsen (1, max_level); + expect_forest_map_consistent (mra); + EXPECT_LE (t8_forest_get_global_num_leaf_elements (mra.get_forest ()), num_adaptive); + + mra.cleanup (); + t8_cmesh_destroy (&cmesh); + t8_scheme_unref (const_cast (&scheme)); +} + +} // namespace + +#endif /* T8_ENABLE_MRA */ diff --git a/test/t8_mra/t8_gtest_mra_cell_geometry.cxx b/test/t8_mra/t8_gtest_mra_cell_geometry.cxx new file mode 100644 index 0000000000..73ff0ba2d0 --- /dev/null +++ b/test/t8_mra/t8_gtest_mra_cell_geometry.cxx @@ -0,0 +1,217 @@ + +#include + +#ifdef T8_ENABLE_MRA + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace +{ + +constexpr double eps = 1e-12; + +/* Cartesian: from_box gives the per-axis affine map [0,1]^DIM <-> physical box */ + +template +void +check_box (const typename t8_mra::cell_geometry::point &lo, + const typename t8_mra::cell_geometry::point &hi) +{ + using geom_t = t8_mra::cell_geometry; + constexpr int DIM = geom_t::DIM; + + const auto geom = geom_t::from_box (lo, hi, 1.0); + + for (const double s : { 0.0, 0.25, 0.5, 1.0 }) { + typename geom_t::point ref; + for (int d = 0; d < DIM; ++d) + ref[d] = s; + + const auto phys = geom.to_physical (ref); + for (int d = 0; d < DIM; ++d) + EXPECT_NEAR (phys[d], lo[d] + ref[d] * (hi[d] - lo[d]), eps); + + const auto back = geom.to_reference (phys); + for (int d = 0; d < DIM; ++d) + EXPECT_NEAR (back[d], ref[d], eps); + + EXPECT_TRUE (geom.contains (phys)); + } +} + +TEST (mra_cell_geometry_cartesian, box_map_all_dims) +{ + check_box ({ 2.0 }, { 5.0 }); + check_box ({ 1.0, 2.0 }, { 4.0, 5.0 }); + check_box ({ 1.0, 2.0, 3.0 }, { 4.0, 6.0, 9.0 }); +} + +TEST (mra_cell_geometry_cartesian, box_map_negative_coordinates) +{ + check_box ({ -2.0, -1.0 }, { 1.0, 0.5 }); +} + +/* Triangle: from_triangle gives the affine map, vertices -> reference corners, and + * physical <-> reference round trips (r0, r1) = barycentric (lambda1, lambda2). */ + +using tri_point = t8_mra::cell_geometry::point; + +void +check_triangle (const tri_point &v0, const tri_point &v1, const tri_point &v2) +{ + using geom_t = t8_mra::cell_geometry; + const auto geom = geom_t::from_triangle (v0, v1, v2, 1.0); + + // Vertices map to the reference corners (0,0), (1,0), (0,1). + const std::array, 3> vertex_ref { + { { v0, { 0.0, 0.0 } }, { v1, { 1.0, 0.0 } }, { v2, { 0.0, 1.0 } } } + }; + for (const auto &[vertex, ref] : vertex_ref) { + const auto got = geom.to_reference (vertex); + EXPECT_NEAR (got[0], ref[0], eps); + EXPECT_NEAR (got[1], ref[1], eps); + } + + // A point built from known barycentric weights inverts to (r0, r1) = (w1, w2). + const std::array, 3> weights { + { { 0.5, 0.3, 0.2 }, { 0.1, 0.6, 0.3 }, { 1.0 / 3, 1.0 / 3, 1.0 / 3 } } + }; + for (const auto &w : weights) { + const tri_point phys { w[0] * v0[0] + w[1] * v1[0] + w[2] * v2[0], w[0] * v0[1] + w[1] * v1[1] + w[2] * v2[1] }; + + const auto ref = geom.to_reference (phys); + EXPECT_NEAR (ref[0], w[1], eps); + EXPECT_NEAR (ref[1], w[2], eps); + + // Forward map reproduces the same physical point. + const auto phys_back = geom.to_physical (ref); + EXPECT_NEAR (phys_back[0], phys[0], eps); + EXPECT_NEAR (phys_back[1], phys[1], eps); + + EXPECT_TRUE (geom.contains (phys)); + } +} + +TEST (mra_cell_geometry_triangle, affine_map_over_many_triangles) +{ + check_triangle ({ 0.0, 0.0 }, { 1.0, 0.0 }, { 0.0, 1.0 }); // reference + check_triangle ({ 0.5, 1.0 }, { 2.0, 0.5 }, { 1.0, 3.0 }); // generic + check_triangle ({ 0.5, 1.0 }, { 1.0, 3.0 }, { 2.0, 0.5 }); // reversed winding + check_triangle ({ -1.0, -1.0 }, { 1.0, -2.0 }, { 0.0, 2.0 }); // negative coordinates +} + +/* Constant mode -> constant field. */ +template +void +check_value_constant_mode (const Geom &geom) +{ + std::array coeffs {}; + coeffs[0] = 2.5; + const std::span c (coeffs.data (), Geom::DOF); + + EXPECT_NEAR (geom.value (c, { 0.5, 0.1 }), geom.value (c, { 0.2, 0.3 }), eps); +} + +/* Affine field (P1 modes `lin`): constant gradient, exact value(pA)-value(pB)=grad.(pA-pB). */ +template +void +check_affine_gradient (const Geom &geom, std::array lin) +{ + using point = typename Geom::point; + std::array coeffs {}; + coeffs[0] = 1.0; + coeffs[lin[0]] = 0.3; + coeffs[lin[1]] = -0.2; + const std::span c (coeffs.data (), Geom::DOF); + + const point refA { 0.2, 0.3 }, refB { 0.5, 0.15 }; + const auto gA = geom.gradient (c, refA); + const auto gB = geom.gradient (c, refB); + for (int d = 0; d < Geom::DIM; ++d) + EXPECT_NEAR (gA[d], gB[d], eps); + + const auto pA = geom.to_physical (refA), pB = geom.to_physical (refB); + double rhs = 0.0; + for (int d = 0; d < Geom::DIM; ++d) + rhs += gA[d] * (pA[d] - pB[d]); + EXPECT_NEAR (geom.value (c, refA) - geom.value (c, refB), rhs, eps); +} + +/* reference_direction(v) = to_reference(origin + v). */ +template +void +check_reference_direction (const Geom &geom) +{ + using point = typename Geom::point; + const point v { 0.7, -0.4 }; + const auto got = geom.reference_direction (v); + const auto expected = geom.to_reference ({ geom.origin[0] + v[0], geom.origin[1] + v[1] }); + for (int d = 0; d < Geom::DIM; ++d) + EXPECT_NEAR (got[d], expected[d], eps); +} + +TEST (mra_cell_geometry, value_gradient_reference_direction) +{ + const auto quad = t8_mra::cell_geometry::from_box ({ 1.0, 2.0 }, { 4.0, 6.0 }, 12.0); + const auto tri = t8_mra::cell_geometry::from_triangle ({ 0.5, 1.0 }, { 2.0, 0.5 }, { 1.0, 3.0 }, + 1.625); + + check_value_constant_mode (quad); + check_value_constant_mode (tri); + + check_affine_gradient (quad, { 1, 3 }); // tensor P1 modes: x-linear = 1, y-linear = P = 3 + check_affine_gradient (tri, { 1, 2 }); // Dubiner P1 modes + + check_reference_direction (quad); + check_reference_direction (tri); +} + +/* dg_basis.basis_value / basis_gradient forward to the reference basis */ + +template +void +check_basis_forward (int quad_param, const std::vector &x_ref) +{ + using elem = t8_mra::element_data; + using basis_t = t8_mra::basis; + constexpr int DIM = t8_mra::shape_traits::DIM; + + t8_mra::dg_basis dg_basis (quad_param); + + std::array x {}; + for (int d = 0; d < DIM; ++d) + x[d] = x_ref[d]; + + const auto val = dg_basis.basis_value (x); + const auto ref_val = basis_t::eval (x); + for (std::size_t i = 0; i < val.size (); ++i) + EXPECT_NEAR (val[i], ref_val[i], eps); + + const auto grad = dg_basis.basis_gradient (x); + const auto ref_grad = basis_t::eval_gradient (x); + for (int dir = 0; dir < DIM; ++dir) + for (std::size_t i = 0; i < grad[dir].size (); ++i) + EXPECT_NEAR (grad[dir][i], ref_grad[dir][i], eps); +} + +TEST (mra_dg_basis, value_and_gradient_forward_all_shapes) +{ + check_basis_forward (3, { 0.4 }); + check_basis_forward (3, { 0.3, 0.7 }); + check_basis_forward (3, { 0.3, 0.6, 0.2 }); + check_basis_forward (4, { 0.25, 0.4 }); // r0+r1 < 1 +} + +} // namespace + +#endif // T8_ENABLE_MRA diff --git a/test/t8_mra/t8_gtest_mra_criteria.cxx b/test/t8_mra/t8_gtest_mra_criteria.cxx new file mode 100644 index 0000000000..23c64a7680 --- /dev/null +++ b/test/t8_mra/t8_gtest_mra_criteria.cxx @@ -0,0 +1,258 @@ +#include + +#ifdef T8_ENABLE_MRA + +#include "t8_gtest_mra_forest.hxx" + +#include + +namespace +{ + +using namespace mra_test; + +constexpr double eps = 1e-12; + +template +class mra_criteria: public ::testing::Test {}; + +TYPED_TEST_SUITE (mra_criteria, Configs, ConfigNames); + +/* Uniform max-level grid with computed details: the common starting point for + * every threshold check. */ +template +void +init_and_decompose (Case &c, F &&f) +{ + c.init (std::forward (f)); + c->multiscale_decomposition (0, c.max_level); +} + +/* threshold_scaling_factor is a domain integral clamped to >= 1, and the DG + * projection is linear, so doubling the data doubles the (unclamped) factor. */ +TYPED_TEST (mra_criteria, threshold_scaling_factor_is_clamped_and_linear) +{ + constexpr auto Shape = TypeParam::Shape; + constexpr auto U = TypeParam::U; + constexpr auto P = TypeParam::P; + constexpr auto DIM = TypeParam::DIM; + + const int max_level = (DIM == 3) ? 2 : 3; + + /* Vanishing data -> the clamp pins every component at 1. */ + { + mra_example example (max_level); + example.init (constant_func (1e-9)); + const auto factor = example->threshold_scaling_factor (); + for (auto u = 0u; u < U; ++u) + EXPECT_NEAR (factor[u], 1.0, eps) << "tiny data must clamp to 1, component " << u; + } + + /* Large data -> unclamped, and linear in the amplitude. */ + mra_example c1 (max_level); + mra_example c2 (max_level); + c1.init (constant_func (100.0)); + c2.init (constant_func (200.0)); + + const auto f1 = c1->threshold_scaling_factor (); + const auto f2 = c2->threshold_scaling_factor (); + for (auto u = 0u; u < U; ++u) { + ASSERT_GT (f1[u], 1.0) << "amplitude 100 should exceed the clamp, component " << u; + EXPECT_NEAR (f2[u] / f1[u], 2.0, eps) << "factor must scale linearly, component " << u; + } +} + +/* local_threshold_value depends only on (vol, level). */ +TYPED_TEST (mra_criteria, local_threshold_value_scales_as_sqrt_num_children) +{ + constexpr auto Shape = TypeParam::Shape; + constexpr auto U = TypeParam::U; + constexpr auto P = TypeParam::P; + constexpr auto DIM = TypeParam::DIM; + using LMI = typename t8_mra::multiscale::levelmultiindex; + + const int max_level = (DIM == 3) ? 3 : 4; + const double ratio = std::sqrt (static_cast (LMI::NUM_CHILDREN)); + + mra_example example (max_level); + init_and_decompose (example, jump_func ()); + auto &mra = example.mra; + + std::size_t pairs = 0; + for (const int gamma : { 1, 2, 3 }) + for (auto l = 2u; l <= static_cast (max_level); ++l) + for (const auto &[lmi, detail] : mra.d_map[l]) { + const auto par = LMI::parent (lmi); + if (!mra.d_map.contains (par)) + continue; + ++pairs; + EXPECT_NEAR (mra.local_threshold_value (lmi, gamma), ratio * mra.local_threshold_value (par, gamma), + eps * mra.local_threshold_value (par, gamma)) + << "level " << l << " gamma " << gamma; + } + EXPECT_GT (pairs, 0u) << "decomposition must leave parent/child detail pairs to compare"; +} + +/* scaled_detail_norm divides the raw detail norm by c_scaling componentwise */ +TYPED_TEST (mra_criteria, scaled_detail_norm_respects_c_scaling) +{ + constexpr auto Shape = TypeParam::Shape; + constexpr auto U = TypeParam::U; + constexpr auto P = TypeParam::P; + constexpr auto DIM = TypeParam::DIM; + + const int max_level = (DIM == 3) ? 3 : 4; + + mra_example example (max_level); + init_and_decompose (example, jump_func ()); + auto &mra = example.mra; + + bool has_nonzero = false; + for (auto l = 0u; l <= static_cast (max_level); ++l) + for (const auto &[lmi, detail] : mra.d_map[l]) { + mra.c_scaling.fill (1.0); + const double base = mra.scaled_detail_norm (lmi); + if (base <= eps) + continue; + has_nonzero = true; + + mra.c_scaling.fill (4.0); + EXPECT_NEAR (mra.scaled_detail_norm (lmi), base / 4.0, eps * base) << "level " << l; + } + EXPECT_TRUE (has_nonzero) << "the jump must leave at least one nonzero detail"; +} + +/* Raising the threshold constant can only remove leaves from the significant + * set. */ +TYPED_TEST (mra_criteria, hard_thresholding_significant_set_shrinks_with_threshold) +{ + constexpr auto Shape = TypeParam::Shape; + constexpr auto U = TypeParam::U; + constexpr auto P = TypeParam::P; + constexpr auto DIM = TypeParam::DIM; + + const int max_level = (DIM == 3) ? 3 : 4; + + mra_example example (max_level); + example.init (jump_func ()); + auto &mra = example.mra; + + t8_mra::hard_thresholding low { 0.1, 2 }; + t8_mra::hard_thresholding high { 10.0, 2 }; + low.prepare (mra); + high.prepare (mra); + + mra.multiscale_decomposition (0, max_level); + + std::size_t number_low = 0, number_high = 0; + for (auto l = 0u; l <= static_cast (max_level); ++l) + for (const auto &[lmi, detail] : mra.d_map[l]) { + const bool significant_low = low.significant (mra, lmi); + const bool significant_high = high.significant (mra, lmi); + + number_low += significant_low; + number_high += significant_high; + EXPECT_TRUE (!significant_high || significant_low) + << "a higher threshold must not gain significance, level " << l; + } + EXPECT_LE (number_high, number_low); + EXPECT_GT (number_low, 0u) << "the jump must make some family significant at a low threshold"; +} + +/* harten_prediction partitions families by the two thresholds: refine_children + * above 2^(P+1)*c_thresh*eps, grade_neighbours above c_thresh*eps, none below. + * refine_children thus always implies grading. */ +TYPED_TEST (mra_criteria, harten_classify_matches_thresholds) +{ + constexpr auto Shape = TypeParam::Shape; + constexpr auto U = TypeParam::U; + constexpr auto P = TypeParam::P; + constexpr auto DIM = TypeParam::DIM; + + const int max_level = (DIM == 3) ? 3 : 4; + + mra_example example (max_level); + example.init (jump_func ()); + auto &mra = example.mra; + + t8_mra::harten_prediction crit { 1.0, 2 }; + crit.prepare (mra); + + mra.multiscale_decomposition (0, max_level); + + const auto steep = std::pow (2.0, static_cast (P) + 1); + for (auto l = 0u; l <= static_cast (max_level); ++l) + for (const auto &[lmi, detail] : mra.d_map[l]) { + const auto norm = mra.scaled_detail_norm (lmi); + const auto eps = mra.local_threshold_value (lmi, 2); // c_thresh = 1.0 + const auto flags = crit (mra, lmi); + + EXPECT_EQ (flags.grade_neighbours, norm > eps) << "level " << l; + EXPECT_EQ (flags.refine_children, norm > steep * eps) << "level " << l; + } +} + +/* The hard threshold is sharp: with c_scaling = 1 a family with detail norm N + * is significant iff N > c_thresh * local_threshold_value, so a c_thresh placed + * just below N/ltv keeps it and just above drops it (mirrors multilaepsch's + * eps +/- 1e-10 boundary case). */ +TYPED_TEST (mra_criteria, hard_thresholding_boundary_is_sharp) +{ + constexpr auto Shape = TypeParam::Shape; + constexpr auto U = TypeParam::U; + constexpr auto P = TypeParam::P; + constexpr auto DIM = TypeParam::DIM; + + const int max_level = (DIM == 3) ? 3 : 4; + constexpr int gamma = 2; + + mra_example example (max_level); + init_and_decompose (example, jump_func ()); + auto &mra = example.mra; + mra.c_scaling.fill (1.0); + + bool checked = false; + for (auto l = 0u; l <= static_cast (max_level) && !checked; ++l) + for (const auto &[lmi, detail] : mra.d_map[l]) { + const double norm = mra.scaled_detail_norm (lmi); + const double ltv = mra.local_threshold_value (lmi, gamma); + if (norm <= eps || ltv <= 0.0) + continue; + + t8_mra::hard_thresholding below { (norm / ltv) * (1.0 - 1e-6), gamma }; + t8_mra::hard_thresholding above { (norm / ltv) * (1.0 + 1e-6), gamma }; + EXPECT_TRUE (below.significant (mra, lmi)) << "detail above threshold must stay"; + EXPECT_FALSE (above.significant (mra, lmi)) << "detail below threshold must drop"; + checked = true; + break; + } + EXPECT_TRUE (checked) << "the jump must leave a nonzero detail to straddle"; +} + +/* coarsen/refine only grade around fresh refinement marks, so an adaptive grid + * can carry larger level jumps; balance() restores the 2:1 face balance, after + * which no leaf borders a neighbour more than one level apart (mirrors + * multilaepsch's grading test). */ +TYPED_TEST (mra_criteria, balance_produces_graded_grid) +{ + constexpr auto Shape = TypeParam::Shape; + constexpr auto U = TypeParam::U; + constexpr auto P = TypeParam::P; + constexpr auto DIM = TypeParam::DIM; + + const int max_level = (DIM == 3) ? 4 : 5; + + mra_example example (max_level); + example.init (jump_func ()); + + example->coarsen (0, max_level); + example->refine (0, max_level); + example->balance (); + + expect_grid_graded (example.mra, /*slack=*/1); + expect_forest_map_consistent (example.mra); +} + +} // namespace + +#endif /* T8_ENABLE_MRA */ diff --git a/test/t8_mra/t8_gtest_mra_data.cxx b/test/t8_mra/t8_gtest_mra_data.cxx new file mode 100644 index 0000000000..03cf26dbf5 --- /dev/null +++ b/test/t8_mra/t8_gtest_mra_data.cxx @@ -0,0 +1,376 @@ +#include + +#ifdef T8_ENABLE_MRA + +#include +#include + +#include +#include +#include +#include + +#include +#include + +namespace +{ + +template +struct ShapeConfig +{ + static constexpr t8_eclass Shape = TShape; +}; + +using ShapeConfigs = ::testing::Types, ShapeConfig, + ShapeConfig, ShapeConfig>; + +template +class mra_lmi: public ::testing::Test {}; +TYPED_TEST_SUITE (mra_lmi, ShapeConfigs); + +/* level() reads back the number of jth_child steps taken from a base cell. */ +TYPED_TEST (mra_lmi, level_counts_refinement_steps) +{ + using lmi_t = t8_mra::levelmultiindex; + + lmi_t lmi (7); // arbitrary base cell, well inside BASECELL_BITS + EXPECT_EQ (lmi.level (), 0u); + + for (auto l = 1u; l <= 5u; ++l) { + lmi = lmi_t::jth_child (lmi, 0); + EXPECT_EQ (lmi.level (), l); + } +} + +/* parent(jth_child(lmi, j)) == lmi for every child slot, and the base cell bits + * survive the round trip. */ +TYPED_TEST (mra_lmi, parent_inverts_jth_child) +{ + using lmi_t = t8_mra::levelmultiindex; + constexpr size_t basecell_mask = (static_cast (1) << lmi_t::BASECELL_BITS) - 1; + + lmi_t lmi (13); + for (auto step = 0; step < 4; ++step) { + for (size_t j = 0; j < static_cast (lmi_t::NUM_CHILDREN); ++j) { + const auto child = lmi_t::jth_child (lmi, j); + EXPECT_EQ (child.level (), lmi.level () + 1); + EXPECT_EQ (child.index & basecell_mask, lmi.index & basecell_mask) << "base cell not preserved"; + EXPECT_EQ (lmi_t::parent (child).index, lmi.index) << "parent did not invert child " << j; + } + lmi = lmi_t::jth_child (lmi, 1); + } +} + +/* children() yields NUM_CHILDREN distinct cells, each one level finer and each + * with lmi as parent; the free helpers agree with the static members. */ +TYPED_TEST (mra_lmi, children_are_distinct_and_consistent) +{ + using lmi_t = t8_mra::levelmultiindex; + + const lmi_t parent (5); + const auto kids = lmi_t::children (parent); + EXPECT_EQ (kids.size (), static_cast (lmi_t::NUM_CHILDREN)); + + for (size_t i = 0; i < kids.size (); ++i) { + EXPECT_EQ (kids[i].level (), parent.level () + 1); + EXPECT_EQ (lmi_t::parent (kids[i]).index, parent.index); + for (size_t j = i + 1; j < kids.size (); ++j) + EXPECT_NE (kids[i].index, kids[j].index) << "children " << i << " and " << j << " collide"; + } + + const auto free_kids = t8_mra::children_lmi (parent); + for (size_t i = 0; i < kids.size (); ++i) + EXPECT_EQ (free_kids[i].index, kids[i].index); + EXPECT_EQ (t8_mra::parent_lmi (kids[0]).index, parent.index); +} + +/* The lmi is its own hash (the value used as the dense-map key). */ +TYPED_TEST (mra_lmi, hash_is_the_index) +{ + using lmi_t = t8_mra::levelmultiindex; + const auto lmi = lmi_t::jth_child (lmi_t (9), 1); + EXPECT_EQ (std::hash {}(lmi), lmi.index); +} + +/* operator== compares the packed index (key equality in the dense map). */ +TYPED_TEST (mra_lmi, equality_compares_the_index) +{ + using lmi_t = t8_mra::levelmultiindex; + const auto a = lmi_t::jth_child (lmi_t (6), 0); + const auto same = lmi_t::jth_child (lmi_t (6), 0); + const auto other = lmi_t::jth_child (lmi_t (6), 1); + + EXPECT_TRUE (a == same); + EXPECT_FALSE (a == other); +} + +/* ---- containers: shape-independent, one representative shape ---- */ + +using lmi_q = t8_mra::levelmultiindex; + +TEST (mra_levelindex_map, insert_get_contains_erase) +{ + t8_mra::levelindex_map map (6); + + const auto a = lmi_q::jth_child (lmi_q (2), 0); // level 1 + const auto b = lmi_q::jth_child (a, 3); // level 2 + + EXPECT_FALSE (map.contains (a)); + EXPECT_EQ (map.find (a), nullptr); + + map.insert (a, 11); + map.insert (b, 22); + + EXPECT_TRUE (map.contains (a)); + EXPECT_EQ (map.get (a), 11); + EXPECT_EQ (*map.find (b), 22); + EXPECT_EQ (map.size (), 2u); + EXPECT_EQ (map.size (a.level ()), 1u); + EXPECT_EQ (map.size (b.level ()), 1u); + + /// in-place mutation through find + *map.find (a) = 99; + EXPECT_EQ (map.get (a), 99); + + /// overwrite via insert + map.insert (a, 5); + EXPECT_EQ (map.get (a), 5); + EXPECT_EQ (map.size (), 2u); + + map.erase (a); + EXPECT_FALSE (map.contains (a)); + EXPECT_EQ (map.size (), 1u); + + map.erase_all (); + EXPECT_EQ (map.size (), 0u); +} + +TEST (mra_levelindex_map, erase_level_clears_only_that_level) +{ + t8_mra::levelindex_map map (6); + const auto l1a = lmi_q::jth_child (lmi_q (0), 0); + const auto l1b = lmi_q::jth_child (lmi_q (0), 1); + const auto l2 = lmi_q::jth_child (l1a, 0); + + map.insert (l1a, 1); + map.insert (l1b, 2); + map.insert (l2, 3); + + map.erase (l1a.level ()); + EXPECT_EQ (map.size (l1a.level ()), 0u); + EXPECT_EQ (map.size (l2.level ()), 1u); + EXPECT_TRUE (map.contains (l2)); +} + +/* iterating through a refinement level */ +TEST (mra_levelindex_map, level_view_iterates_that_level) +{ + t8_mra::levelindex_map map (6); + + const auto root = lmi_q (3); + const auto kids = lmi_q::children (root); + + for (size_t i = 0; i < kids.size (); ++i) + map.insert (kids[i], static_cast (10 + i)); + + map.insert (lmi_q::jth_child (kids[0], 0), 99); // a level-2 cell + + const unsigned int level = 1; + EXPECT_EQ (map[level].size (), kids.size ()); + + size_t counted = 0; + for (auto it = map.begin (level); it != map.end (level); ++it) { + EXPECT_EQ (it->first.level (), level); + EXPECT_TRUE (map.contains (it->first)); + ++counted; + } + EXPECT_EQ (counted, kids.size ()); +} + +/* The (level, key) overloads agree with the lmi overloads. */ +TEST (mra_levelindex_map, level_key_overloads_match_lmi_overloads) +{ + t8_mra::levelindex_map map (6); + const auto a = lmi_q::jth_child (lmi_q (4), 2); + + map.insert (a.level (), a.index, 7); + EXPECT_TRUE (map.contains (a.level (), a.index)); + EXPECT_TRUE (map.contains (a)); + EXPECT_EQ (map.get (a.level (), a.index), 7); + EXPECT_EQ (map.get (a), 7); + + map.erase (a.level (), a.index); + EXPECT_FALSE (map.contains (a)); +} + +TEST (mra_levelindex_set, insert_contains_erase_sizes) +{ + t8_mra::levelindex_set set (6); + const auto a = lmi_q::jth_child (lmi_q (1), 2); + const auto b = lmi_q::jth_child (a, 1); + + set.insert (a); + set.insert (b); + set.insert (a); // idempotent + + EXPECT_TRUE (set.contains (a)); + EXPECT_TRUE (set.contains (b)); + EXPECT_EQ (set.size (), 2u); + EXPECT_EQ (set.size (a.level ()), 1u); + + set.erase (b); + EXPECT_FALSE (set.contains (b)); + EXPECT_EQ (set.size (), 1u); + + set.erase_all (); + EXPECT_EQ (set.size (), 0u); +} + +TEST (mra_levelindex_set, level_view_and_level_key_overloads) +{ + t8_mra::levelindex_set set (6); + const auto kids = lmi_q::children (lmi_q (2)); + for (const auto &k : kids) + set.insert (k); + + const unsigned int level = 1; + EXPECT_EQ (set[level].size (), kids.size ()); + + size_t counted = 0; + for (auto it = set.begin (level); it != set.end (level); ++it) + ++counted; + EXPECT_EQ (counted, kids.size ()); + + const auto a = kids[0]; + EXPECT_TRUE (set.contains (a.level (), a.index)); + set.erase (a.level (), a.index); + EXPECT_FALSE (set.contains (a)); +} + +/* ---- element_data ---- */ + +TEST (mra_element_data, dg_idx_is_a_bijection) +{ + using elem = t8_mra::element_data; // U=3, DOF = 4*4 = 16 + constexpr auto U = elem::U_DIM; + constexpr auto DOF = elem::DOF; + + std::array seen {}; + for (size_t u = 0; u < U; ++u) + for (size_t p = 0; p < DOF; ++p) { + const auto idx = elem::dg_idx (u, p); + ASSERT_LT (idx, U * DOF); + EXPECT_FALSE (seen[idx]) << "dg_idx collision at (" << u << "," << p << ")"; + seen[idx] = true; + } +} + +TEST (mra_element_data, wavelet_idx_is_a_bijection) +{ + using det = t8_mra::detail_data; + constexpr auto U = det::U_DIM; + constexpr auto DOF = det::DOF; + constexpr auto NC = det::NUM_CHILDREN; + + std::array seen {}; + for (size_t k = 0; k < NC; ++k) + for (size_t u = 0; u < U; ++u) + for (size_t p = 0; p < DOF; ++p) { + const auto idx = det::wavelet_idx (k, u, p); + ASSERT_LT (idx, NC * U * DOF); + EXPECT_FALSE (seen[idx]) << "wavelet_idx collision at (" << k << "," << u << "," << p << ")"; + seen[idx] = true; + } +} + +/* The repartition/ghost wire ships element_data as raw bytes. */ +TEST (mra_element_data, is_trivially_copyable) +{ + EXPECT_TRUE ((std::is_trivially_copyable_v>) ); + EXPECT_TRUE ((std::is_trivially_copyable_v>) ); +} + +/* triangle_order: TRIANGLE-specific Bey vertex-order tables (pure) */ + +/* The six permutations of three vertices. */ +const std::array, 6> all_orders + = { { { 0, 1, 2 }, { 0, 2, 1 }, { 1, 0, 2 }, { 1, 2, 0 }, { 2, 0, 1 }, { 2, 1, 0 } } }; + +template +bool +is_permutation (const std::array &p) +{ + std::array seen {}; + for (int v : p) { + if (v < 0 || v >= static_cast (N) || seen[v]) + return false; + seen[v] = true; + } + return true; +} + +/* invert_order produces the true inverse permutation q (q[p[i]] = i). */ +TEST (mra_triangle_order, invert_order_is_the_permutation_inverse) +{ + for (const auto &order : all_orders) { + std::array expected {}; + for (int i = 0; i < 3; ++i) + expected[order[i]] = i; + + auto tmp_order = order; + t8_mra::triangle_order::invert_order (tmp_order); + EXPECT_EQ (tmp_order, expected) << "wrong inverse for {" << order[0] << order[1] << order[2] << "}"; + } +} + +/* Inverting twice is the identity. */ +TEST (mra_triangle_order, invert_order_is_an_involution) +{ + for (const auto &order : all_orders) { + auto tmp_order = order; + t8_mra::triangle_order::invert_order (tmp_order); + t8_mra::triangle_order::invert_order (tmp_order); + EXPECT_EQ (tmp_order, order); + } +} + +/* get_point_order and get_parent_order map a permutation to a permutation; the + * identity order with child 0 stays the identity. */ +TEST (mra_triangle_order, order_maps_stay_permutations) +{ + for (const auto &order : all_orders) + for (int bey = 0; bey < 4; ++bey) { + auto tmp_order = order; + t8_mra::triangle_order::get_point_order (tmp_order, bey); + EXPECT_TRUE (is_permutation<3> (tmp_order)) << "get_point_order broke the permutation"; + } + + std::array id { 0, 1, 2 }; + t8_mra::triangle_order::get_point_order (id, 0); + EXPECT_EQ ((std::array { 0, 1, 2 }), id); + + for (const auto &order : all_orders) { + auto tmp_order = order; + t8_mra::triangle_order::get_parent_order (tmp_order); + EXPECT_TRUE (is_permutation<3> (tmp_order)) << "get_parent_order broke the permutation"; + } +} + +/* For a fixed parent order and Bey type, child_id -> reference index is a + * bijection over {0,1,2,3} (the four children land on four distinct slots). */ +TEST (mra_triangle_order, reference_children_order_is_a_bijection) +{ + for (int type = 1; type <= 2; ++type) + for (const auto &order : all_orders) { + std::array ref {}; + for (int child = 0; child < 4; ++child) + ref[child] = t8_mra::triangle_order::get_reference_children_order (type, child, order); + EXPECT_TRUE (is_permutation<4> (ref)) + << "type " << type << " order {" << order[0] << order[1] << order[2] << "} not a bijection"; + } +} + +} // namespace + +#endif // T8_ENABLE_MRA diff --git a/test/t8_mra/t8_gtest_mra_forest.hxx b/test/t8_mra/t8_gtest_mra_forest.hxx new file mode 100644 index 0000000000..30ce562935 --- /dev/null +++ b/test/t8_mra/t8_gtest_mra_forest.hxx @@ -0,0 +1,340 @@ +#ifndef T8_GTEST_MRA_FOREST_HXX +#define T8_GTEST_MRA_FOREST_HXX + +#ifdef T8_ENABLE_MRA + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace mra_test +{ + +template +struct Config +{ + static constexpr t8_eclass Shape = TShape; + static constexpr int U = U_; + static constexpr int P = P_; + static constexpr int DIM = t8_mra::shape_traits::DIM; +}; + +using Configs + = ::testing::Types, Config, Config, + Config, Config, Config, + Config, Config, + Config, Config, Config, + Config, Config, Config, + Config, Config, Config, + Config>; + +struct ConfigNames +{ + template + static std::string + GetName (int) + { + return std::string (t8_eclass_to_string[T::Shape]) + "_U" + std::to_string (T::U) + "_P" + std::to_string (T::P); + } +}; + +/* Smooth test function */ +template +auto +smooth_func () +{ + if constexpr (DIM == 1) + return [] (double x) { + std::array res; + for (auto u = 0; u < U; ++u) + res[u] = std::sin (2.0 * M_PI * (u + 1) * x); + return res; + }; + else if constexpr (DIM == 2) + return [] (double x, double y) { + std::array res; + for (auto u = 0; u < U; ++u) + res[u] = std::sin (2.0 * M_PI * (u + 1) * x) * std::sin (2.0 * M_PI * y); + return res; + }; + else + return [] (double x, double y, double z) { + std::array res; + for (auto u = 0; u < U; ++u) + res[u] = std::sin (2.0 * M_PI * (u + 1) * x) * std::sin (2.0 * M_PI * y) * std::sin (2.0 * M_PI * z); + return res; + }; +} + +/* Discontinuous test function: jump along a circle/sphere segment. Each side is + * a polynomial of total degree P-1, hence exactly representable at order P */ +template +auto +jump_func () +{ + constexpr int d = P - 1; + if constexpr (DIM == 1) + return [] (double x) { + std::array res; + const double r = x * x; + const double in = std::pow (0.5 + x, d); + const double out = std::pow (0.3 + x, d); + for (auto u = 0; u < U; ++u) + res[u] = (u + 1) * ((r < 0.25) ? (3.0 + in) : out); + return res; + }; + else if constexpr (DIM == 2) + return [] (double x, double y) { + std::array res; + const double r = x * x + y * y; + const double in = std::pow (0.5 + x + y, d); + const double out = std::pow (0.3 + x - y, d); + for (auto u = 0; u < U; ++u) + res[u] = (u + 1) * ((r < 0.25) ? (3.0 + in) : out); + return res; + }; + else + return [] (double x, double y, double z) { + std::array res; + const double r = x * x + y * y + z * z; + const double in = std::pow (0.5 + x + y + z, d); + const double out = std::pow (0.3 + x - y + z, d); + for (auto u = 0; u < U; ++u) + res[u] = (u + 1) * ((r < 0.25) ? (3.0 + in) : out); + return res; + }; +} + +/* Constant initial data: all details vanish exactly. */ +template +auto +constant_func (double amplitude = 3.0) +{ + if constexpr (DIM == 1) + return [amplitude] (double) { + std::array res; + for (auto u = 0; u < U; ++u) + res[u] = amplitude * (u + 1); + return res; + }; + else if constexpr (DIM == 2) + return [amplitude] (double, double) { + std::array res; + for (auto u = 0; u < U; ++u) + res[u] = amplitude * (u + 1); + return res; + }; + else + return [amplitude] (double, double, double) { + std::array res; + for (auto u = 0; u < U; ++u) + res[u] = amplitude * (u + 1); + return res; + }; +} + +/* Single-piece polynomial of total degree P-1, hence exactly representable in + * the order-P space on every shape (Dubiner: total degree <= P-1; tensor + * Legendre: per-variable degree <= P-1). No jump, so a correct projection + * leaves zero details. */ +template +auto +poly_func () +{ + constexpr int d = P - 1; + if constexpr (DIM == 1) + return [] (double x) { + std::array res; + const double base = std::pow (0.5 + 0.3 * x, d); + for (auto u = 0; u < U; ++u) + res[u] = (u + 1) * base; + return res; + }; + else if constexpr (DIM == 2) + return [] (double x, double y) { + std::array res; + const double base = std::pow (0.5 + 0.3 * x + 0.4 * y, d); + for (auto u = 0; u < U; ++u) + res[u] = (u + 1) * base; + return res; + }; + else + return [] (double x, double y, double z) { + std::array res; + const double base = std::pow (0.5 + 0.3 * x + 0.4 * y + 0.2 * z, d); + for (auto u = 0; u < U; ++u) + res[u] = (u + 1) * base; + return res; + }; +} + +/* Every forest leaf must have its lmi stored with the element's level, and + * exactly the leaves must be present in lmi_map. */ +template +void +expect_forest_map_consistent (MRA &mra) +{ + auto *forest = mra.get_forest (); + auto *user_data = mra.get_user_data (); + auto *lmi_map = mra.get_lmi_map (); + const auto *scheme = t8_forest_get_scheme (forest); + + ASSERT_EQ (static_cast (t8_forest_get_local_num_leaf_elements (forest)), lmi_map->size ()); + + auto current_idx = t8_locidx_t { 0 }; + const auto num_trees = t8_forest_get_num_local_trees (forest); + for (t8_locidx_t tree_idx = 0; tree_idx < num_trees; ++tree_idx) { + const auto tree_class = t8_forest_get_tree_class (forest, tree_idx); + const auto num_elements = t8_forest_get_tree_num_leaf_elements (forest, tree_idx); + + for (t8_locidx_t ele_idx = 0; ele_idx < num_elements; ++ele_idx, ++current_idx) { + const auto *element = t8_forest_get_leaf_element_in_tree (forest, tree_idx, ele_idx); + const auto lmi = t8_mra::get_lmi_from_forest_data (user_data, current_idx); + + EXPECT_EQ (lmi.level (), static_cast (scheme->element_get_level (tree_class, element))); + EXPECT_TRUE (lmi_map->contains (lmi)); + } + } +} + +template +void +expect_maps_equal (const MapT &expected, const MapT &actual, unsigned int max_level, double tol) +{ + for (auto l = 0u; l <= max_level; ++l) { + ASSERT_EQ (expected[l].size (), actual[l].size ()) << "entry count differs on level " << l; + + for (const auto &[lmi, data] : expected[l]) { + ASSERT_TRUE (actual.contains (lmi)) << "missing lmi on level " << l; + + const auto &other = actual.get (lmi); + ASSERT_EQ (data.u_coeffs.size (), other.u_coeffs.size ()); + for (auto i = 0u; i < data.u_coeffs.size (); ++i) + EXPECT_NEAR (data.u_coeffs[i], other.u_coeffs[i], tol) << "coeff " << i << " on level " << l; + } + } +} + +/* (cmesh, scheme, multiscale) triple on a unit hypercube */ +template +class mra_example { + public: + using multiscale = t8_mra::multiscale; + using levelmultiindex = typename multiscale::levelmultiindex; + static constexpr int DIM = t8_mra::shape_traits::DIM; + + explicit mra_example (int max_level, sc_MPI_Comm comm = sc_MPI_COMM_WORLD) + : mra (max_level, comm), max_level (max_level) + { + cmesh = t8_cmesh_new_hypercube (TShape, comm, 0, 0, 0); + scheme = t8_scheme_new_default (); + t8_cmesh_ref (cmesh); + t8_scheme_ref (const_cast (scheme)); + } + + ~mra_example () + { + mra.cleanup (); + t8_cmesh_destroy (&cmesh); + t8_scheme_unref (const_cast (&scheme)); + } + + mra_example (const mra_example &) = delete; + + mra_example & + operator= (const mra_example &) = delete; + + template + void + init (F &&f) + { + mra.initialize_data (cmesh, scheme, max_level, std::forward (f)); + } + + template + void + init_adaptive (F &&f) + { + mra.initialize_data_adaptive (cmesh, scheme, max_level, std::forward (f)); + } + + multiscale * + operator->() + { + return &mra; + } + + multiscale & + operator* () + { + return mra; + } + + multiscale mra; + t8_cmesh_t cmesh; + const t8_scheme *scheme; + int max_level; +}; + +/* Grading invariant: across every face the leaf levels differ by at most + * `slack` (slack 1 = the 2:1 graded grid). Domain-boundary faces carry no + * neighbour and are skipped. Serial only -- a process boundary without a ghost + * layer would also report zero neighbours. */ +template +void +expect_grid_graded (MRA &mra, int slack = 1) +{ + auto *forest = mra.get_forest (); + const auto *scheme = t8_forest_get_scheme (forest); + const auto num_trees = t8_forest_get_num_local_trees (forest); + + for (t8_locidx_t tree = 0; tree < num_trees; ++tree) { + const auto tree_class = t8_forest_get_tree_class (forest, tree); + const auto num_elements = t8_forest_get_tree_num_leaf_elements (forest, tree); + + for (t8_locidx_t e = 0; e < num_elements; ++e) { + const auto *element = t8_forest_get_leaf_element_in_tree (forest, tree, e); + const int level = scheme->element_get_level (tree_class, element); + const int num_faces = scheme->element_get_num_faces (tree_class, element); + + for (int face = 0; face < num_faces; ++face) { + const t8_element_t **neighbors = nullptr; + int *dual_faces = nullptr; + t8_locidx_t *element_indices = nullptr; + int num_neighbors = 0; + t8_eclass_t neigh_class; + + t8_forest_leaf_face_neighbors (forest, tree, element, &neighbors, face, &dual_faces, &num_neighbors, + &element_indices, &neigh_class); + + for (int n = 0; n < num_neighbors; ++n) { + const int neigh_level = scheme->element_get_level (neigh_class, neighbors[n]); + EXPECT_LE (std::abs (level - neigh_level), slack) << "ungraded face neighbour at level " << level; + } + + if (num_neighbors > 0) { + T8_FREE (neighbors); + T8_FREE (element_indices); + T8_FREE (dual_faces); + } + } + } + } +} + +} // namespace mra_test + +#endif /* T8_ENABLE_MRA */ + +#endif /* T8_GTEST_MRA_FOREST_HXX */ diff --git a/test/t8_mra/t8_gtest_mra_mpi.cxx b/test/t8_mra/t8_gtest_mra_mpi.cxx new file mode 100644 index 0000000000..7849d752bc --- /dev/null +++ b/test/t8_mra/t8_gtest_mra_mpi.cxx @@ -0,0 +1,262 @@ +#include + +#ifdef T8_ENABLE_MRA + +#include "t8_gtest_mra_forest.hxx" + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +using namespace mra_test; + +using MpiConfigs = ::testing::Types, Config, + Config, Config, + Config, Config, + Config, Config>; + +template +class mra_mpi: public ::testing::Test {}; + +TYPED_TEST_SUITE (mra_mpi, MpiConfigs, ConfigNames); + +/* (lmi.index -> per-component means) map of this rank's leaves. */ +template +std::map> +local_leaf_means (MRA &mra) +{ + std::map> means; + + auto *user_data = mra.get_user_data (); + auto *lmi_map = mra.get_lmi_map (); + mra.for_each_local_leaf ([&] (t8_locidx_t, const t8_element_t *, unsigned int local_idx, t8_gloidx_t) { + const auto lmi = t8_mra::get_lmi_from_forest_data (user_data, local_idx); + const auto mean = mra.mean_val (lmi_map->get (lmi)); + means[lmi.index] = std::vector (mean.begin (), mean.end ()); + }); + + return means; +} + +/* The full (lmi.index -> per-component means) map over all ranks, identical on + * every rank. */ +template +std::map> +global_leaf_means (MRA &mra) +{ + constexpr int U = MRA::U_DIM; + const auto local = local_leaf_means (mra); + + int mpisize = 1; + sc_MPI_Comm_size (sc_MPI_COMM_WORLD, &mpisize); + + int n_local = static_cast (local.size ()); + std::vector keys; + std::vector vals; + keys.reserve (n_local); + vals.reserve (static_cast (n_local) * U); + for (const auto &[key, value] : local) { + keys.push_back (static_cast (key)); + vals.insert (vals.end (), value.begin (), value.end ()); + } + + std::vector counts (mpisize), displs (mpisize, 0); + sc_MPI_Allgather (&n_local, 1, sc_MPI_INT, counts.data (), 1, sc_MPI_INT, sc_MPI_COMM_WORLD); + for (int r = 1; r < mpisize; ++r) + displs[r] = displs[r - 1] + counts[r - 1]; + const int total = displs[mpisize - 1] + counts[mpisize - 1]; + + std::vector all_keys (total); + sc_MPI_Allgatherv (keys.data (), n_local, T8_MPI_GLOIDX, all_keys.data (), counts.data (), displs.data (), + T8_MPI_GLOIDX, sc_MPI_COMM_WORLD); + + // Values carry U doubles per leaf: scale the counts and offsets by U. + std::vector vcounts (mpisize), vdispls (mpisize, 0); + for (int r = 0; r < mpisize; ++r) + vcounts[r] = counts[r] * U; + for (int r = 1; r < mpisize; ++r) + vdispls[r] = vdispls[r - 1] + vcounts[r - 1]; + + std::vector all_vals (static_cast (total) * U); + sc_MPI_Allgatherv (vals.data (), n_local * U, sc_MPI_DOUBLE, all_vals.data (), vcounts.data (), vdispls.data (), + sc_MPI_DOUBLE, sc_MPI_COMM_WORLD); + + std::map> global; + for (int i = 0; i < total; ++i) + global[static_cast (all_keys[i])] + = std::vector (all_vals.begin () + i * U, all_vals.begin () + (i + 1) * U); + return global; +} + +/* Domain integral per component (sum of mean * vol) over the mra's + * communicator. */ +template +auto +domain_integral (MRA &mra) +{ + constexpr auto U = MRA::U_DIM; + std::array local = {}; + + auto *lmi_map = mra.get_lmi_map (); + mra.for_each_local_leaf ([&] (t8_locidx_t, const t8_element_t *, unsigned int local_idx, t8_gloidx_t) { + const auto lmi = t8_mra::get_lmi_from_forest_data (mra.get_user_data (), local_idx); + const auto &data = lmi_map->get (lmi); + const auto mean = mra.mean_val (data); + for (auto u = 0u; u < U; ++u) + local[u] += mean[u] * data.vol; + }); + + std::array global = {}; + sc_MPI_Allreduce (local.data (), global.data (), U, sc_MPI_DOUBLE, sc_MPI_SUM, mra.get_comm ()); + return global; +} + +/* Cross-run reference for build-time proc-independence: a single-rank run writes + * the global map, a multi-rank run reads it and compares. Ordered by the ctest + * fixture (serial FIXTURES_SETUP, parallel FIXTURES_REQUIRED). */ +std::string +ref_path (const std::string &name) +{ + return "/tmp/mra_mpi_ref_" + name + ".txt"; +} + +void +write_ref (const std::string &name, const std::map> &means) +{ + std::ofstream file (ref_path (name)); + file << std::setprecision (17); + for (const auto &[key, values] : means) { + file << key; + for (const auto value : values) + file << ' ' << value; + file << '\n'; + } +} + +std::map> +read_ref (const std::string &name) +{ + std::ifstream file (ref_path (name)); + std::map> means; + std::string line; + while (std::getline (file, line)) { + std::istringstream iss (line); + std::size_t key; + if (!(iss >> key)) + continue; + std::vector values; + double value; + while (iss >> value) + values.push_back (value); + means[key] = values; + } + return means; +} + +/* Cross-rank-count teeth: a single-rank run writes the global map as reference, + * a multi-rank run asserts its own global map is identical. Catches a criterion + * that builds a partition-dependent grid or data (e.g. a rank-local + * normalization that was never reduced). */ +template +void +expect_matches_serial_ref (MRA &mra, const std::string &name) +{ + const auto global = global_leaf_means (mra); + + int mpisize = 1, mpirank = 0; + sc_MPI_Comm_size (sc_MPI_COMM_WORLD, &mpisize); + sc_MPI_Comm_rank (sc_MPI_COMM_WORLD, &mpirank); + if (mpisize == 1) { + if (mpirank == 0) + write_ref (name, global); + return; + } + + const auto ref = read_ref (name); + ASSERT_FALSE (ref.empty ()) << "reference " << ref_path (name) << " missing; run t8_gtest_mra_mpi_serial first"; + ASSERT_EQ (global.size (), ref.size ()) << "built a different leaf count on " << mpisize << " ranks"; + for (const auto &[key, values] : ref) { + const auto it = global.find (key); + ASSERT_NE (it, global.end ()) << "leaf " << key << " missing when built on " << mpisize << " ranks"; + ASSERT_EQ (it->second.size (), values.size ()) << "leaf " << key << " component count changed"; + for (std::size_t u = 0; u < values.size (); ++u) + EXPECT_EQ (it->second[u], values[u]) + << "leaf " << key << " component " << u << " differs when built on " << mpisize << " ranks"; + } +} + +/* The bottom-up adaptive initialization must build the same grid and data + * regardless of the rank count (the postponed proc-dependence regression + * check). A single-rank run writes the reference, a multi-rank run compares its + * global map against it. */ +TYPED_TEST (mra_mpi, partition_independent_adaptive_init) +{ + constexpr auto Shape = TypeParam::Shape; + constexpr auto U = TypeParam::U; + constexpr auto P = TypeParam::P; + constexpr auto DIM = TypeParam::DIM; + + const int max_level = (DIM == 3) ? 3 : 4; + + mra_example world (max_level); + world.init_adaptive (jump_func ()); + + expect_matches_serial_ref (world.mra, ConfigNames::GetName (0) + "_adaptive_init"); +} + +/* A criterion-driven static adapt (threshold coarsen + prediction refine + + * balance) must build the same grid and data regardless of the rank count. The + * cross-count teeth for the coarsening and refinement criteria: a rank-local + * normalization (e.g. an un-reduced max) would build a different grid here. */ +TYPED_TEST (mra_mpi, partition_independent_static_adapt) +{ + constexpr auto Shape = TypeParam::Shape; + constexpr auto U = TypeParam::U; + constexpr auto P = TypeParam::P; + constexpr auto DIM = TypeParam::DIM; + + const int max_level = (DIM == 3) ? 3 : 4; + + mra_example world (max_level); + world.init (jump_func ()); + world->coarsen (0, max_level); + world->refine (0, max_level); + world->balance (); + + expect_matches_serial_ref (world.mra, ConfigNames::GetName (0) + "_static_adapt"); +} + +/* The domain integral survives a coarsen/refine/balance cycle and its + * repartition. */ +TYPED_TEST (mra_mpi, domain_integral_conserved_across_repartition) +{ + constexpr auto Shape = TypeParam::Shape; + constexpr auto U = TypeParam::U; + constexpr auto P = TypeParam::P; + constexpr auto DIM = TypeParam::DIM; + + const int max_level = (DIM == 3) ? 3 : 4; + + mra_example mra (max_level); + mra.init (jump_func ()); + + const auto before = domain_integral (mra.mra); + mra->coarsen (0, max_level); + mra->refine (0, max_level); + mra->balance (); + const auto after = domain_integral (mra.mra); + + for (auto u = 0u; u < U; ++u) + EXPECT_NEAR (after[u], before[u], 1e-10) << "component " << u; +} + +} // namespace + +#endif /* T8_ENABLE_MRA */ diff --git a/test/t8_mra/t8_gtest_mra_mst.cxx b/test/t8_mra/t8_gtest_mra_mst.cxx new file mode 100644 index 0000000000..d952f6f497 --- /dev/null +++ b/test/t8_mra/t8_gtest_mra_mst.cxx @@ -0,0 +1,549 @@ +#include + +#ifdef T8_ENABLE_MRA + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace +{ + +constexpr double eps = 1e-12; + +template +struct MstConfig +{ + static constexpr t8_eclass Shape = TShape; + static constexpr int P = P_; +}; + +using MstConfigs + = ::testing::Types, MstConfig, MstConfig, + MstConfig, MstConfig, MstConfig, + MstConfig, MstConfig, + MstConfig>; + +template +class mra_mst: public ::testing::Test { + public: + static constexpr t8_eclass Shape = Config::Shape; + static constexpr unsigned short P = Config::P; + + using element_t = t8_mra::element_data; + using mst_t = t8_mra::mst; + using detail_t = typename mst_t::detail_t; + using lmi_t = t8_mra::levelmultiindex; + + static constexpr unsigned int NUM_CHILDREN = lmi_t::NUM_CHILDREN; + static constexpr unsigned int U = element_t::U_DIM; + static constexpr unsigned int DOF = element_t::DOF; + + std::vector mask; + + void + SetUp () override + { + t8_mra::compute_mask (mask); + } + + static element_t + make_leaf (std::size_t seed, double vol = 1.0) + { + element_t e; + e.vol = vol; + e.order = { 0, 1, 2 }; + for (unsigned int u = 0; u < U; ++u) + for (unsigned int i = 0; i < DOF; ++i) + e.u_coeffs[element_t::dg_idx (u, i)] = std::sin (0.7 * (seed + 1) + 1.3 * u + 2.1 * i); + return e; + } +}; +TYPED_TEST_SUITE (mra_mst, MstConfigs); + +/* decomposition then inverse returns the original single family. */ +TYPED_TEST (mra_mst, round_trip_identity_one_level) +{ + using element_t = typename TestFixture::element_t; + using detail_t = typename TestFixture::detail_t; + using mst_t = typename TestFixture::mst_t; + using lmi_t = typename TestFixture::lmi_t; + constexpr auto NUM_CHILDREN = TestFixture::NUM_CHILDREN; + constexpr auto DOF = TestFixture::DOF; + constexpr auto U = TestFixture::U; + + t8_mra::levelindex_map lmi_map (3); + t8_mra::levelindex_map d_map (3); + + const auto base = lmi_t (0); + const auto kids = lmi_t::children (base); + std::array orig; + for (unsigned int k = 0; k < NUM_CHILDREN; ++k) { + orig[k] = TestFixture::make_leaf (k); + lmi_map.insert (kids[k], orig[k]); + } + + mst_t::multiscale_decomposition (0, 1, lmi_map, d_map, this->mask); + ASSERT_EQ (lmi_map.size (1), 0u) << "family not collapsed"; + ASSERT_TRUE (lmi_map.contains (base)); + + mst_t::inverse_multiscale_transformation (0, 1, lmi_map, d_map, this->mask); + ASSERT_EQ (lmi_map.size (1), NUM_CHILDREN); + + for (unsigned int k = 0; k < NUM_CHILDREN; ++k) { + const auto &got = lmi_map.get (kids[k]); + EXPECT_NEAR (got.vol, orig[k].vol, eps); + for (unsigned int u = 0; u < U; ++u) + for (unsigned int i = 0; i < DOF; ++i) + EXPECT_NEAR (got.u_coeffs[element_t::dg_idx (u, i)], orig[k].u_coeffs[element_t::dg_idx (u, i)], eps) + << "child " << k << " comp " << u << " dof " << i; + } +} + +/* Two-level cascade: collapse NC^2 leaves to the base, reconstruct, compare. */ +TYPED_TEST (mra_mst, round_trip_identity_two_levels) +{ + using element_t = typename TestFixture::element_t; + using detail_t = typename TestFixture::detail_t; + using mst_t = typename TestFixture::mst_t; + using lmi_t = typename TestFixture::lmi_t; + constexpr auto NUM_CHILDREN = TestFixture::NUM_CHILDREN; + constexpr auto DOF = TestFixture::DOF; + constexpr auto U = TestFixture::U; + + t8_mra::levelindex_map lmi_map (3); + t8_mra::levelindex_map d_map (3); + + const auto base = lmi_t (0); + const auto kids = lmi_t::children (base); + + std::vector leaves; + std::vector orig; + std::size_t seed = 0; + for (unsigned int a = 0; a < NUM_CHILDREN; ++a) + for (const auto &g : lmi_t::children (kids[a])) { + const auto e = TestFixture::make_leaf (seed++); + lmi_map.insert (g, e); + leaves.push_back (g); + orig.push_back (e); + } + + mst_t::multiscale_decomposition (0, 2, lmi_map, d_map, this->mask); + ASSERT_EQ (lmi_map.size (2), 0u); + ASSERT_EQ (lmi_map.size (1), 0u); + ASSERT_TRUE (lmi_map.contains (base)); + + mst_t::inverse_multiscale_transformation (0, 2, lmi_map, d_map, this->mask); + ASSERT_EQ (lmi_map.size (2), leaves.size ()); + + for (std::size_t n = 0; n < leaves.size (); ++n) { + const auto &got = lmi_map.get (leaves[n]); + for (unsigned int u = 0; u < U; ++u) + for (unsigned int i = 0; i < DOF; ++i) + EXPECT_NEAR (got.u_coeffs[element_t::dg_idx (u, i)], orig[n].u_coeffs[element_t::dg_idx (u, i)], eps) + << "leaf " << n << " comp " << u << " dof " << i; + } +} + +/* Conservation: the integral of the parent equals the sum of the children + * integrals. The cell integral is mean * vol; the constant-mode coefficient + * gives the mean as u_const for the cartesian basis but u_const / sqrt(vol) + * for the triangle (Dubiner) basis, so the per-cell integral is + * Q = u_const * (cartesian ? vol : sqrt(vol)). */ +TYPED_TEST (mra_mst, two_scale_conserves_mass) +{ + using element_t = typename TestFixture::element_t; + using detail_t = typename TestFixture::detail_t; + using mst_t = typename TestFixture::mst_t; + constexpr auto Shape = TestFixture::Shape; + constexpr auto NUM_CHILDREN = TestFixture::NUM_CHILDREN; + constexpr auto U = TestFixture::U; + + const auto integral + = [] (double u_const, double vol) { return u_const * (t8_mra::is_cartesian ? vol : std::sqrt (vol)); }; + + std::array sib; + for (unsigned int k = 0; k < NUM_CHILDREN; ++k) + sib[k] = TestFixture::make_leaf (k); + + detail_t parent; + mst_t::two_scale_family (sib, parent, this->mask); + + for (unsigned int u = 0; u < U; ++u) { + double child_int = 0.0; + for (unsigned int k = 0; k < NUM_CHILDREN; ++k) + child_int += integral (sib[k].u_coeffs[element_t::dg_idx (u, 0)], sib[k].vol); + const double parent_int = integral (parent.u_coeffs[element_t::dg_idx (u, 0)], parent.vol); + EXPECT_NEAR (parent_int, child_int, eps) << "component " << u; + } +} + +/* Wavelet kernel: data that lives exactly on the coarse space (a parent with + * zero details, prolonged to the children) produces zero details when + * re-analysed, and the parent is recovered. */ +TYPED_TEST (mra_mst, details_vanish_on_coarse_representable_data) +{ + using element_t = typename TestFixture::element_t; + using detail_t = typename TestFixture::detail_t; + using mst_t = typename TestFixture::mst_t; + using lmi_t = typename TestFixture::lmi_t; + constexpr auto NUM_CHILDREN = TestFixture::NUM_CHILDREN; + constexpr auto DOF = TestFixture::DOF; + constexpr auto U = TestFixture::U; + + t8_mra::levelindex_map lmi_map (3); + t8_mra::levelindex_map d_map (3); + + const auto base = lmi_t (0); + + /// A coarse parent with arbitrary scaling coefficients and no detail. + detail_t parent; + parent.vol = NUM_CHILDREN; // children get vol 1 + parent.order = { 0, 1, 2 }; + for (unsigned int u = 0; u < U; ++u) + for (unsigned int i = 0; i < DOF; ++i) + parent.u_coeffs[element_t::dg_idx (u, i)] = std::cos (0.4 + 1.1 * u + 0.9 * i); + + lmi_map.insert (base, static_cast (parent)); + d_map.insert (base, parent); // d_coeffs default to zero + + /// Prolong to the children: they now represent a pure coarse function. + mst_t::inverse_multiscale_transformation (0, 1, lmi_map, d_map, this->mask); + ASSERT_EQ (lmi_map.size (1), NUM_CHILDREN); + + /// Re-analyse: details must vanish, parent scaling coefficients recovered. + mst_t::multiscale_decomposition (0, 1, lmi_map, d_map, this->mask); + ASSERT_TRUE (lmi_map.contains (base)); + + const auto &re = d_map.get (base); + for (unsigned int k = 0; k < NUM_CHILDREN; ++k) + for (unsigned int u = 0; u < U; ++u) + for (unsigned int i = 0; i < DOF; ++i) + EXPECT_NEAR (re.d_coeffs[detail_t::wavelet_idx (k, u, i)], 0.0, eps) + << "spurious detail at child " << k << " comp " << u << " dof " << i; + + const auto &got = lmi_map.get (base); + for (unsigned int u = 0; u < U; ++u) + for (unsigned int i = 0; i < DOF; ++i) + EXPECT_NEAR (got.u_coeffs[element_t::dg_idx (u, i)], parent.u_coeffs[element_t::dg_idx (u, i)], eps); +} + +/* Vanishing moments */ +TYPED_TEST (mra_mst, details_vanish_for_projected_polynomial) +{ + using element_t = typename TestFixture::element_t; + using detail_t = typename TestFixture::detail_t; + using mst_t = typename TestFixture::mst_t; + constexpr auto Shape = TestFixture::Shape; + constexpr auto NUM_CHILDREN = TestFixture::NUM_CHILDREN; + constexpr auto P = TestFixture::P; + constexpr auto DOF = TestFixture::DOF; + constexpr auto U = TestFixture::U; + constexpr unsigned int DIM = element_t::DIM; + + using basis_t = t8_mra::basis; + const t8_mra::quadrature quad (t8_mra::quadrature::rule_for_degree (2 * P)); + const auto children = t8_mra::child_maps (); + constexpr double ref_volume = t8_mra::is_cartesian ? 1.0 : 0.5; + + /// Affine form on the parent reference element, raised to P-1: total degree + /// P-1. + const auto poly = [] (const std::array &eta, unsigned int u) { + const double coeff[3] = { 0.6, 0.4, 0.2 }; + double a = 0.3; + for (unsigned int d = 0; d < DIM; ++d) + a += coeff[d] * eta[d]; + return static_cast (u + 1) * std::pow (a, static_cast (P) - 1); + }; + + /// Project the global polynomial onto each child's basis + std::array sib; + for (unsigned int k = 0; k < NUM_CHILDREN; ++k) { + sib[k].vol = 1.0; + sib[k].order = { 0, 1, 2 }; + for (unsigned int u = 0; u < U; ++u) { + std::array c {}; + for (std::size_t q = 0; q < quad.num_points; ++q) { + std::array xi {}; + for (unsigned int d = 0; d < DIM; ++d) + xi[d] = quad.points[DIM * q + d]; + const auto phi = basis_t::eval (xi); + const double f = poly (children[k](xi), u); + const double w = quad.weights[q]; + for (unsigned int i = 0; i < DOF; ++i) + c[i] += ref_volume * w * f * phi[i]; + } + for (unsigned int i = 0; i < DOF; ++i) + sib[k].u_coeffs[element_t::dg_idx (u, i)] = c[i]; + } + } + + detail_t parent; + mst_t::two_scale_family (sib, parent, this->mask); + + for (double d : parent.d_coeffs) + EXPECT_NEAR (d, 0.0, eps) << "non-vanishing detail for a degree <= P-1 polynomial"; +} + +/* Order-P cancellation */ +TYPED_TEST (mra_mst, details_scale_as_h_to_the_P) +{ + using element_t = typename TestFixture::element_t; + using detail_t = typename TestFixture::detail_t; + using mst_t = typename TestFixture::mst_t; + constexpr auto Shape = TestFixture::Shape; + constexpr auto NUM_CHILDREN = TestFixture::NUM_CHILDREN; + constexpr auto P = TestFixture::P; + constexpr auto DOF = TestFixture::DOF; + constexpr auto U = TestFixture::U; + constexpr unsigned int DIM = element_t::DIM; + + using basis_t = t8_mra::basis; + const t8_mra::quadrature quad (t8_mra::quadrature::rule_for_degree (2 * P)); + const auto children = t8_mra::child_maps (); + constexpr double ref_volume = t8_mra::is_cartesian ? 1.0 : 0.5; + + /// f(x) = (sum_d x_d)^P : degree exactly P. + const auto f = [] (const std::array &x) { + double s = 0.0; + for (unsigned int d = 0; d < DIM; ++d) + s += x[d]; + return std::pow (s, static_cast (P)); + }; + + /// L2 detail norm of f projected onto a family on a parent of physical size h. + const auto detail_norm = [&] (double h) { + std::array sib; + for (unsigned int k = 0; k < NUM_CHILDREN; ++k) { + sib[k].vol = 1.0; + sib[k].order = { 0, 1, 2 }; + for (unsigned int u = 0; u < U; ++u) { + std::array c {}; + for (std::size_t q = 0; q < quad.num_points; ++q) { + std::array xi {}; + for (unsigned int d = 0; d < DIM; ++d) + xi[d] = quad.points[DIM * q + d]; + const auto phi = basis_t::eval (xi); + const auto eta = children[k](xi); // child ref -> parent ref + std::array x {}; + for (unsigned int d = 0; d < DIM; ++d) + x[d] = h * eta[d]; // parent ref -> physical (size h) + const double val = f (x); + const double w = quad.weights[q]; + for (unsigned int i = 0; i < DOF; ++i) + c[i] += ref_volume * w * val * phi[i]; + } + for (unsigned int i = 0; i < DOF; ++i) + sib[k].u_coeffs[element_t::dg_idx (u, i)] = c[i]; + } + } + detail_t parent; + mst_t::two_scale_family (sib, parent, this->mask); + double s = 0.0; + for (double d : parent.d_coeffs) + s += d * d; + return std::sqrt (s); + }; + + const double D1 = detail_norm (1.0); + const double D2 = detail_norm (0.5); + EXPECT_GT (D1, eps) << "a degree-P polynomial must leave a detail (it is not in the coarse space)"; + EXPECT_NEAR (D2 / D1, std::pow (0.5, static_cast (P)), eps) + << "details must decay as h^P (order-P cancellation)"; +} + +/* Round trip on a graded (adaptive) grid: one branch refined a level deeper + * than the rest. */ +TYPED_TEST (mra_mst, round_trip_identity_graded_grid) +{ + using element_t = typename TestFixture::element_t; + using detail_t = typename TestFixture::detail_t; + using mst_t = typename TestFixture::mst_t; + using lmi_t = typename TestFixture::lmi_t; + constexpr auto NUM_CHILDREN = TestFixture::NUM_CHILDREN; + constexpr auto DOF = TestFixture::DOF; + constexpr auto U = TestFixture::U; + + t8_mra::levelindex_map lmi_map (3); + t8_mra::levelindex_map d_map (3); + + const auto base = lmi_t (0); + const auto kids = lmi_t::children (base); // level 1 + + std::vector leaves; + std::vector orig; + std::size_t seed = 0; + + /// kids[0] is refined one level deeper: its NUM_CHILDREN children are level-2 leaves. + for (const auto &g : lmi_t::children (kids[0])) { + const auto e = TestFixture::make_leaf (seed++, 1.0); + lmi_map.insert (g, e); + leaves.push_back (g); + orig.push_back (e); + } + + /// The remaining children of the base stay as level-1 leaves (vol NUM_CHILDREN times the + /// level-2 cells, the consistent grading). + for (unsigned int k = 1; k < NUM_CHILDREN; ++k) { + const auto e = TestFixture::make_leaf (seed++, static_cast (NUM_CHILDREN)); + lmi_map.insert (kids[k], e); + leaves.push_back (kids[k]); + orig.push_back (e); + } + + mst_t::multiscale_decomposition (0, 2, lmi_map, d_map, this->mask); + ASSERT_TRUE (lmi_map.contains (base)); + ASSERT_EQ (lmi_map.size (), 1u) << "graded grid not fully collapsed to the base"; + + mst_t::inverse_multiscale_transformation (0, 2, lmi_map, d_map, this->mask); + ASSERT_EQ (lmi_map.size (), leaves.size ()); + + for (std::size_t n = 0; n < leaves.size (); ++n) { + ASSERT_TRUE (lmi_map.contains (leaves[n])) << "missing leaf " << n; + const auto &got = lmi_map.get (leaves[n]); + for (unsigned int u = 0; u < U; ++u) + for (unsigned int i = 0; i < DOF; ++i) + EXPECT_NEAR (got.u_coeffs[element_t::dg_idx (u, i)], orig[n].u_coeffs[element_t::dg_idx (u, i)], eps) + << "leaf " << n << " comp " << u << " dof " << i; + } +} + +/* Sanity guard for the vanishing test: generic (non-coarse) data must produce + * at least one non-trivial detail */ +TYPED_TEST (mra_mst, generic_data_produces_nonzero_details) +{ + using element_t = typename TestFixture::element_t; + using detail_t = typename TestFixture::detail_t; + using mst_t = typename TestFixture::mst_t; + constexpr auto NUM_CHILDREN = TestFixture::NUM_CHILDREN; + + if constexpr (TestFixture::DOF == 1) { + GTEST_SKIP () << "P=1 has only the constant mode; no detail can be non-zero"; + } + else { + std::array sib; + for (unsigned int k = 0; k < NUM_CHILDREN; ++k) + sib[k] = TestFixture::make_leaf (k); + + detail_t parent; + mst_t::two_scale_family (sib, parent, this->mask); + + double max_detail = 0.0; + for (double d : parent.d_coeffs) + max_detail = std::max (max_detail, std::abs (d)); + EXPECT_GT (max_detail, eps); + } +} + +/* The non-destructive analysis keeps the leaves in lmi_map and writes the + * parent (scaling coeffs + details) into d_map; the details equal a direct + * two_scale_family of the same children. */ +TYPED_TEST (mra_mst, multiscale_transformation_is_non_destructive) +{ + using element_t = typename TestFixture::element_t; + using detail_t = typename TestFixture::detail_t; + using mst_t = typename TestFixture::mst_t; + using lmi_t = typename TestFixture::lmi_t; + constexpr auto NUM_CHILDREN = TestFixture::NUM_CHILDREN; + constexpr auto DOF = TestFixture::DOF; + constexpr auto U = TestFixture::U; + + t8_mra::levelindex_map lmi_map (3); + t8_mra::levelindex_map d_map (3); + + const auto base = lmi_t (0); + const auto kids = lmi_t::children (base); + std::array sib; + for (unsigned int k = 0; k < NUM_CHILDREN; ++k) { + sib[k] = TestFixture::make_leaf (k); + lmi_map.insert (kids[k], sib[k]); + } + + mst_t::multiscale_transformation (0, 1, lmi_map, d_map, this->mask); + + /// Leaves untouched, parent not inserted. + EXPECT_EQ (lmi_map.size (1), NUM_CHILDREN); + EXPECT_FALSE (lmi_map.contains (base)); + for (unsigned int k = 0; k < NUM_CHILDREN; ++k) + EXPECT_TRUE (lmi_map.contains (kids[k])); + + /// d_map holds the parent, equal to a direct two-scale of the same family. + ASSERT_TRUE (d_map.contains (base)); + detail_t direct; + mst_t::two_scale_family (sib, direct, this->mask); + const auto &got = d_map.get (base); + for (unsigned int u = 0; u < U; ++u) + for (unsigned int i = 0; i < DOF; ++i) { + EXPECT_NEAR (got.u_coeffs[element_t::dg_idx (u, i)], direct.u_coeffs[element_t::dg_idx (u, i)], eps); + for (unsigned int k = 0; k < NUM_CHILDREN; ++k) + EXPECT_NEAR (got.d_coeffs[detail_t::wavelet_idx (k, u, i)], direct.d_coeffs[detail_t::wavelet_idx (k, u, i)], + eps); + } +} + +/* An incomplete family (a missing sibling) is skipped by both the analysis and + * the destructive collapse: no parent appears, the present leaves stay. */ +TYPED_TEST (mra_mst, incomplete_family_is_skipped) +{ + using element_t = typename TestFixture::element_t; + using detail_t = typename TestFixture::detail_t; + using mst_t = typename TestFixture::mst_t; + using lmi_t = typename TestFixture::lmi_t; + constexpr auto NUM_CHILDREN = TestFixture::NUM_CHILDREN; + + const auto base = lmi_t (0); + const auto kids = lmi_t::children (base); + + // Only NUM_CHILDREN-1 of the NUM_CHILDREN siblings are present. + auto build = [&] (t8_mra::levelindex_map &lmi_map) { + for (unsigned int k = 0; k < NUM_CHILDREN - 1; ++k) + lmi_map.insert (kids[k], TestFixture::make_leaf (k)); + }; + + { + t8_mra::levelindex_map lmi_map (3); + t8_mra::levelindex_map d_map (3); + build (lmi_map); + mst_t::multiscale_transformation (0, 1, lmi_map, d_map, this->mask); + EXPECT_FALSE (d_map.contains (base)) << "incomplete family produced a detail"; + EXPECT_EQ (lmi_map.size (1), NUM_CHILDREN - 1); + } + { + t8_mra::levelindex_map lmi_map (3); + t8_mra::levelindex_map d_map (3); + build (lmi_map); + mst_t::multiscale_decomposition (0, 1, lmi_map, d_map, this->mask); + EXPECT_FALSE (lmi_map.contains (base)) << "incomplete family collapsed"; + EXPECT_EQ (lmi_map.size (1), NUM_CHILDREN - 1) << "present leaves must remain"; + } +} + +/* Scaling policy values per shape. */ +TYPED_TEST (mra_mst, scaling_policy_values) +{ + constexpr auto Shape = TestFixture::Shape; + constexpr auto NUM_CHILDREN = TestFixture::NUM_CHILDREN; + using policy = t8_mra::mst_scaling_policy; + + EXPECT_EQ (policy::inverse_scaling_factor (), 1.0); + if constexpr (t8_mra::is_cartesian) + EXPECT_EQ (policy::forward_scaling_factor (NUM_CHILDREN), 1.0 / static_cast (NUM_CHILDREN)); + else + EXPECT_EQ (policy::forward_scaling_factor (NUM_CHILDREN), 1.0); +} + +} // namespace + +#endif // T8_ENABLE_MRA diff --git a/test/t8_mra/t8_gtest_mra_num.cxx b/test/t8_mra/t8_gtest_mra_num.cxx new file mode 100644 index 0000000000..c86dde7e6c --- /dev/null +++ b/test/t8_mra/t8_gtest_mra_num.cxx @@ -0,0 +1,484 @@ +#include + +#ifdef T8_ENABLE_MRA + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace +{ + +constexpr double eps = 1e-12; + +template +struct NumConfig +{ + static constexpr t8_eclass Shape = TShape; + static constexpr int P = P_; + static constexpr int DIM = t8_mra::shape_traits::DIM; +}; + +using NumConfigs + = ::testing::Types, NumConfig, NumConfig, + NumConfig, NumConfig, NumConfig, + NumConfig, NumConfig, + NumConfig>; + +/// Volume of the reference cell: +/// 1 for the cartesian [0,1]^DIM +/// 1/2 for the reference triangle +template +inline constexpr double ref_volume = t8_mra::is_cartesian ? 1.0 : 0.5; + +/// Points strictly inside the reference cell +template +std::vector> +interior_points () +{ + std::vector> pts; + if constexpr (t8_mra::is_cartesian) { + std::array a; + a.fill (0.3); + std::array b; + b.fill (0.55); + pts.push_back (a); + pts.push_back (b); + if constexpr (DIM >= 2) + pts.push_back ([] { + std::array c; + c.fill (0.4); + c[0] = 0.2; + c[1] = 0.7; + return c; + }()); + } + else { + pts.push_back ({ 0.25, 0.25 }); + pts.push_back ({ 0.5, 0.3 }); + pts.push_back ({ 0.2, 0.6 }); + } + return pts; +} + +template +class mra_num: public ::testing::Test { + public: + static constexpr t8_eclass Shape = Config::Shape; + static constexpr int P = Config::P; + static constexpr int DIM = Config::DIM; + static constexpr int DOF = t8_mra::shape_traits::dof (P); + + using basis_t = t8_mra::basis; + + /// Exact to degree >= 2(P-1) on every shape + t8_mra::quadrature quad { t8_mra::quadrature::rule_for_degree (2 * P) }; +}; +TYPED_TEST_SUITE (mra_num, NumConfigs); + +/* Weights are positive, sum to one (normalized rule) */ +TYPED_TEST (mra_num, quadrature_weights_and_points) +{ + constexpr auto Shape = TestFixture::Shape; + constexpr int DIM = TestFixture::DIM; + const auto &quad = this->quad; + + double wsum = 0.0; + for (std::size_t i = 0; i < quad.num_points; ++i) { + EXPECT_GT (quad.weights[i], 0.0); + wsum += quad.weights[i]; + + if constexpr (t8_mra::is_cartesian) { + for (int d = 0; d < DIM; ++d) { + EXPECT_GE (quad.points[DIM * i + d], 0.0); + EXPECT_LE (quad.points[DIM * i + d], 1.0); + } + } + else { + const double x = quad.points[2 * i + 0]; + const double y = quad.points[2 * i + 1]; + EXPECT_GE (x, 0.0); + EXPECT_GE (y, 0.0); + EXPECT_LE (x + y, 1.0 + eps); + } + } + EXPECT_NEAR (wsum, 1.0, eps); +} + +/* Polynomial exactness: cartesian integrates x0^(2n-1) = 1/(2n) exactly; + * triangle integrates x^a y^b = a! b! / (a+b+2)! exactly up to the rule degree. */ +TYPED_TEST (mra_num, quadrature_is_exact) +{ + constexpr auto Shape = TestFixture::Shape; + constexpr int DIM = TestFixture::DIM; + constexpr int P = TestFixture::P; + const auto &quad = this->quad; + + if constexpr (t8_mra::is_cartesian) { + const int n = t8_mra::quadrature::rule_for_degree (2 * P); // points per axis + const int m = 2 * n - 1; // highest exactly integrable degree + + /// Single-axis monomial x0^m. + double axis = 0.0; + for (std::size_t i = 0; i < quad.num_points; ++i) + axis += quad.weights[i] * std::pow (quad.points[DIM * i + 0], m); + EXPECT_NEAR (axis, 1.0 / (m + 1), eps); + + /// Full-dimensional monomial prod_d x_d^m: exercises the tensor weight + /// product across every axis. Exact value (1/(m+1))^DIM. + double mixed = 0.0; + for (std::size_t i = 0; i < quad.num_points; ++i) { + double f = quad.weights[i]; + for (int d = 0; d < DIM; ++d) + f *= std::pow (quad.points[DIM * i + d], m); + mixed += f; + } + EXPECT_NEAR (mixed, std::pow (1.0 / (m + 1), DIM), eps); + } + else { + const auto mono = [&] (int a, int b) { + double s = 0.0; + for (std::size_t i = 0; i < quad.num_points; ++i) + s += quad.weights[i] * std::pow (quad.points[2 * i + 0], a) * std::pow (quad.points[2 * i + 1], b); + return s; + }; + const auto fact = [] (int k) { + double f = 1.0; + for (int i = 2; i <= k; ++i) + f *= i; + return f; + }; + const auto exact = [&] (int a, int b) { return fact (a) * fact (b) / fact (a + b + 2); }; + + /// integral over the reference triangle = ref_volume * sum_q w_q f(x_q) + EXPECT_NEAR (ref_volume * mono (0, 0), exact (0, 0), eps); // area = 1/2 + EXPECT_NEAR (ref_volume * mono (1, 0), exact (1, 0), eps); // 1/6 + EXPECT_NEAR (ref_volume * mono (1, 1), exact (1, 1), eps); // 1/24 + } +} + +/* The reference basis is orthonormal */ +TYPED_TEST (mra_num, basis_is_orthonormal) +{ + constexpr auto Shape = TestFixture::Shape; + constexpr int DIM = TestFixture::DIM; + constexpr int DOF = TestFixture::DOF; + using basis_t = typename TestFixture::basis_t; + const auto &quad = this->quad; + + for (int i = 0; i < DOF; ++i) + for (int j = 0; j < DOF; ++j) { + double mij = 0.0; + + for (std::size_t k = 0; k < quad.num_points; ++k) { + std::array x {}; + for (int d = 0; d < DIM; ++d) + x[d] = quad.points[DIM * k + d]; + const auto phi = basis_t::eval (x); + mij += quad.weights[k] * phi[i] * phi[j]; + } + + mij *= ref_volume; // integral over the reference cell + EXPECT_NEAR (mij, (i == j) ? 1.0 : 0.0, eps) << "mass(" << i << "," << j << ")"; + } +} + +/* eval_gradient matches the analytic gradient of a known polynomial. + * + * The basis spans every polynomial of degree <= P-1, so the monomial sum + * u(x) = sum_d x_d^(P-1) + * is represented exactly by its projection coefficients c_i = integral u phi_i. + */ +TYPED_TEST (mra_num, gradient_matches_exact_polynomial) +{ + constexpr auto Shape = TestFixture::Shape; + constexpr int DIM = TestFixture::DIM; + constexpr int DOF = TestFixture::DOF; + constexpr int P = TestFixture::P; + using basis_t = typename TestFixture::basis_t; + const auto &quad = this->quad; + + const auto u = [] (const std::array &x) { + double s = 0.0; + for (int d = 0; d < DIM; ++d) + s += std::pow (x[d], P - 1); + return s; + }; + const auto grad_u + = [] (const std::array &x, int dir) { return (P >= 2) ? (P - 1) * std::pow (x[dir], P - 2) : 0.0; }; + + std::array c {}; + for (std::size_t q = 0; q < quad.num_points; ++q) { + std::array xq {}; + + for (int d = 0; d < DIM; ++d) + xq[d] = quad.points[DIM * q + d]; + + const auto phi = basis_t::eval (xq); + const double uw = ref_volume * quad.weights[q] * u (xq); + + for (int i = 0; i < DOF; ++i) + c[i] += uw * phi[i]; + } + + for (const auto &x : interior_points ()) { + const auto grad = basis_t::eval_gradient (x); + for (int dir = 0; dir < DIM; ++dir) { + double recon = 0.0; + for (int i = 0; i < DOF; ++i) + recon += c[i] * grad[dir][i]; + EXPECT_NEAR (recon, grad_u (x, dir), eps) << "dir " << dir; + } + } +} + +/* Two-scale refinement equation: the parent basis evaluated at the child-mapped + * point equals a mask-weighted sum of child basis functions, + * phi_j(Phi_k xi) = (ref_vol / mask_norm) * sum_i mask_k(i,j) phi_i(xi). + */ +TYPED_TEST (mra_num, mask_satisfies_refinement_equation) +{ + constexpr auto Shape = TestFixture::Shape; + constexpr int P = TestFixture::P; + constexpr int DIM = TestFixture::DIM; + constexpr int DOF = TestFixture::DOF; + using basis_t = typename TestFixture::basis_t; + + std::vector mask; + t8_mra::compute_mask (mask); + + const auto children = t8_mra::child_maps (); + const double factor = ref_volume / t8_mra::mask_policy::norm; + + for (std::size_t k = 0; k < children.size (); ++k) + for (const auto &xi : interior_points ()) { + const auto phi_child = basis_t::eval (xi); + const auto phi_parent = basis_t::eval (children[k](xi)); + for (int j = 0; j < DOF; ++j) { + double rhs = 0.0; + for (int i = 0; i < DOF; ++i) + rhs += mask[k](i, j) * phi_child[i]; + EXPECT_NEAR (phi_parent[j], factor * rhs, eps) << "child " << k << " parent dof " << j; + } + } +} + +/* ---- geometry: reference -> physical cartesian map ---- */ +TEST (mra_geometry, deref_1d_maps_endpoints_and_midpoint) +{ + EXPECT_NEAR (t8_mra::deref_1d (0.0, 2.0, 5.0), 2.0, eps); + EXPECT_NEAR (t8_mra::deref_1d (1.0, 2.0, 5.0), 5.0, eps); + EXPECT_NEAR (t8_mra::deref_1d (0.5, 2.0, 5.0), 3.5, eps); +} + +TEST (mra_geometry, deref_maps_box_corners) +{ + const std::array lo { -1.0, 3.0 }, hi { 1.0, 4.0 }; + EXPECT_EQ (t8_mra::deref<2> ({ 0.0, 0.0 }, lo, hi), lo); + EXPECT_EQ (t8_mra::deref<2> ({ 1.0, 1.0 }, lo, hi), hi); + const auto mid = t8_mra::deref<2> ({ 0.5, 0.5 }, lo, hi); + EXPECT_NEAR (mid[0], 0.0, eps); + EXPECT_NEAR (mid[1], 3.5, eps); +} + +/* extract_cartesian_vertices picks the lower corner (index 0) and the upper + * corner (index 2 for a 2D cell) out of the t8code vertex layout. */ +TEST (mra_geometry, extract_cartesian_vertices_picks_min_max) +{ + /// QUAD physical vertices + const std::array, 4> verts + = { { { 1.0, 2.0, 0.0 }, { 3.0, 2.0, 0.0 }, { 4.0, 5.0, 0.0 }, { 3.0, 5.0, 0.0 } } }; + std::array lo {}, hi {}; + + t8_mra::extract_cartesian_vertices<2> (verts, lo, hi); + EXPECT_EQ (lo, (std::array { 1.0, 2.0 })); + EXPECT_EQ (hi, (std::array { 4.0, 5.0 })); +} + +/* transform_quad_points maps flattened reference points onto the physical box. */ +TEST (mra_geometry, transform_quad_points_maps_onto_box) +{ + const std::array lo { 1.0, 2.0 }, hi { 4.0, 5.0 }; + const std::vector ref { 0.0, 0.0, 1.0, 1.0, 0.5, 0.5 }; // 3 points + const auto phys = t8_mra::transform_quad_points<2> (ref, 3, lo, hi); + + ASSERT_EQ (phys.size (), 6u); + EXPECT_NEAR (phys[0], 1.0, eps); // (0,0) -> lo + EXPECT_NEAR (phys[1], 2.0, eps); + EXPECT_NEAR (phys[2], 4.0, eps); // (1,1) -> hi + EXPECT_NEAR (phys[3], 5.0, eps); + EXPECT_NEAR (phys[4], 2.5, eps); // (0.5,0.5) -> centre + EXPECT_NEAR (phys[5], 3.5, eps); +} + +/* ---- mat: dense matrix and its LU solver ---- */ +TEST (mra_mat, element_access_and_resize) +{ + t8_mra::mat A (2, 3); + EXPECT_EQ (A.rows (), 2u); + EXPECT_EQ (A.cols (), 3u); + + A (1, 2) = 7.0; + A (0, 0) = -1.0; + EXPECT_EQ (A (1, 2), 7.0); + EXPECT_EQ (A (0, 0), -1.0); + EXPECT_EQ (A (0, 1), 0.0); // value-initialized +#if T8_ENABLE_DEBUG + EXPECT_THROW (A (2, 0), std::out_of_range); // bounds check is debug-only +#endif + + A.resize (4, 4); + EXPECT_EQ (A.rows (), 4u); + EXPECT_EQ (A.cols (), 4u); + EXPECT_EQ (A (3, 3), 0.0); // resize clears +} + +/* LU factor + solve recovers the known solution of A x = b (with pivoting). */ +TEST (mra_mat, lu_solve_recovers_known_solution) +{ + /// Non-symmetric, well-conditioned 3x3; row order forces a pivot. + t8_mra::mat A (3, 3, { 0.0, 2.0, 1.0, 1.0, 3.0, -1.0, 2.0, 1.0, 4.0 }); + const std::vector x_true { 1.0, -2.0, 3.0 }; + + std::vector b (3, 0.0); + for (size_t i = 0; i < 3; ++i) + for (size_t j = 0; j < 3; ++j) + b[i] += A (i, j) * x_true[j]; + + std::vector p; + std::vector x = b; + t8_mra::lu_factors (A, p); // A becomes its LU factors, p the pivot order + t8_mra::lu_solve (A, p, x); + + for (size_t i = 0; i < 3; ++i) + EXPECT_NEAR (x[i], x_true[i], eps) << "component " << i; +} + +/* ---- nodal <-> modal converters ---- */ +using ConvConfigs + = ::testing::Types, NumConfig, NumConfig, + NumConfig, NumConfig>; + +template +class mra_nodal_modal: public ::testing::Test { + public: + static constexpr t8_eclass Shape = Config::Shape; + static constexpr int P = Config::P; + static constexpr int DIM = Config::DIM; + static constexpr int DOF = t8_mra::shape_traits::dof (P); + + using basis_t = t8_mra::basis; + using node_set = std::array, DOF>; + + /// Tensor P distinct 1D nodes onto DOF cell nodes (basis-index decomposition, + /// first coordinate fastest); any distinct set makes the Vandermonde + /// nonsingular. + static node_set + tensor_nodes (const std::array &x1d) + { + node_set nd {}; + for (int p = 0; p < DOF; ++p) { + int idx = p; + for (int d = 0; d < DIM; ++d) { + nd[p][d] = x1d[idx % P]; + idx /= P; + } + } + return nd; + } + + /// Uniform (equispaced) and nonuniform (Chebyshev-Lobatto, clustered at the + /// ends) node grids: the converters must handle both. + std::vector node_sets = { + tensor_nodes ([] { + std::array x {}; + for (int i = 0; i < P; ++i) + x[i] = i / static_cast (P - 1); + return x; + }()), + tensor_nodes ([] { + std::array x {}; + for (int i = 0; i < P; ++i) + x[i] = 0.5 * (1.0 - std::cos (M_PI * i / (P - 1))); + return x; + }()), + }; +}; +TYPED_TEST_SUITE (mra_nodal_modal, ConvConfigs); + +/* modal -> nodal -> modal is the identity (square Vandermonde interpolation), + * with two components to cover the component-major layout. */ +TYPED_TEST (mra_nodal_modal, roundtrip_recovers_modal_coefficients) +{ + constexpr auto Shape = TestFixture::Shape; + constexpr int P = TestFixture::P; + constexpr int DOF = TestFixture::DOF; + constexpr unsigned int U = 2; + + for (const auto &nodes : this->node_sets) { + const t8_mra::nodal_to_modal to_modal (nodes); + const t8_mra::modal_to_nodal to_nodal (nodes); + + std::array modal {}; + for (unsigned int i = 0; i < U * DOF; ++i) + modal[i] = std::sin (0.7 * i + 0.3) + 0.5; + + const auto nodal = to_nodal (std::span (modal.data (), modal.size ())); + const auto back = to_modal (std::span (nodal.data (), nodal.size ())); + + for (unsigned int i = 0; i < U * DOF; ++i) + EXPECT_NEAR (back[i], modal[i], eps) << "coeff " << i; + } +} + +/* Nodal interpolation is exact for polynomials in the basis span: sampling + * u(x) = sum_d x_d^(P-1) at the nodes and converting to modal reconstructs u + * everywhere. */ +TYPED_TEST (mra_nodal_modal, interpolation_is_exact_for_polynomials) +{ + constexpr auto Shape = TestFixture::Shape; + constexpr int P = TestFixture::P; + constexpr int DIM = TestFixture::DIM; + constexpr int DOF = TestFixture::DOF; + using basis_t = typename TestFixture::basis_t; + + const auto u = [] (const std::array &x) { + double s = 0.0; + for (int d = 0; d < DIM; ++d) + s += std::pow (x[d], P - 1); + return s; + }; + + for (const auto &nodes : this->node_sets) { + std::array nodal {}; + for (int j = 0; j < DOF; ++j) + nodal[j] = u (nodes[j]); + + const t8_mra::nodal_to_modal to_modal (nodes); + const auto modal = to_modal (std::span (nodal.data (), nodal.size ())); + + for (const auto &x : interior_points ()) { + const auto phi = basis_t::eval (x); + double recon = 0.0; + for (int i = 0; i < DOF; ++i) + recon += modal[i] * phi[i]; // cartesian normalization is 1 + EXPECT_NEAR (recon, u (x), eps); + } + } +} + +} // namespace + +#endif // T8_ENABLE_MRA diff --git a/test/t8_mra/t8_gtest_mra_projection.cxx b/test/t8_mra/t8_gtest_mra_projection.cxx new file mode 100644 index 0000000000..4b0b6c0e51 --- /dev/null +++ b/test/t8_mra/t8_gtest_mra_projection.cxx @@ -0,0 +1,428 @@ +#include + +#ifdef T8_ENABLE_MRA + +#include "t8_gtest_mra_forest.hxx" + +#include + +#include +#include + +namespace +{ + +using namespace mra_test; + +constexpr double eps = 1e-12; + +/* Sample points guaranteed inside (or on) a leaf: every vertex plus the + * centroid. A degree <= P-1 polynomial is reproduced exactly at all of them. */ +template +std::vector> +leaf_sample_points (t8_forest_t forest, t8_locidx_t tree_idx, const t8_element_t *element, int num_vertices) +{ + std::vector> samples; + std::array centroid = {}; + + for (int v = 0; v < num_vertices; ++v) { + double coord[3] = {}; + t8_forest_element_coordinate (forest, tree_idx, element, v, coord); + + std::array point; + for (auto d = 0; d < DIM; ++d) { + point[d] = coord[d]; + centroid[d] += coord[d] / num_vertices; + } + samples.push_back (point); + } + samples.push_back (centroid); + + return samples; +} + +/* Call an (x,y[,z]) test function with a point stored as an array. */ +template +auto +eval_func (F &&f, const std::array &x) +{ + if constexpr (DIM == 1) + return f (x[0]); + else if constexpr (DIM == 2) + return f (x[0], x[1]); + else + return f (x[0], x[1], x[2]); +} + +template +class mra_projection: public ::testing::Test {}; + +TYPED_TEST_SUITE (mra_projection, Configs, ConfigNames); + +/* Constant data: only the constant mode survives on every leaf. */ +TYPED_TEST (mra_projection, constant_projects_to_pure_constant_mode) +{ + constexpr auto Shape = TypeParam::Shape; + constexpr auto U = TypeParam::U; + constexpr auto P = TypeParam::P; + constexpr auto DIM = TypeParam::DIM; + using element_t = typename t8_mra::multiscale::element_t; + + const int max_level = (DIM == 3) ? 2 : 3; + + mra_example example (max_level); + example.init (constant_func (2.5)); + auto &mra = example.mra; + + auto *lmi_map = mra.get_lmi_map (); + for (auto l = 0u; l <= static_cast (max_level); ++l) + for (const auto &[lmi, data] : (*lmi_map)[l]) + for (auto u = 0u; u < U; ++u) + for (auto i = 1u; i < element_t::DOF; ++i) + EXPECT_NEAR (data.u_coeffs[element_t::dg_idx (u, i)], 0.0, eps) + << "non-constant mode " << i << " must vanish for constant data, component " << u; +} + +/* Degree <= P-1 polynomial lives in the coarse space: a full decomposition + * leaves zero details (projection exactness through the forest path; on + * triangles also the derived vertex ordering). */ +TYPED_TEST (mra_projection, representable_polynomial_has_no_details) +{ + constexpr auto Shape = TypeParam::Shape; + constexpr auto U = TypeParam::U; + constexpr auto P = TypeParam::P; + constexpr auto DIM = TypeParam::DIM; + using detail_t = typename t8_mra::multiscale::detail_t; + + const int max_level = (DIM == 3) ? 3 : 4; + + mra_example example (max_level); + example.init (poly_func ()); + auto &mra = example.mra; + + mra.multiscale_decomposition (0, max_level); + + std::size_t families = 0; + for (auto l = 0u; l <= static_cast (max_level); ++l) + for (const auto &[lmi, detail] : mra.d_map[l]) { + ++families; + for (const double d : detail.d_coeffs) + EXPECT_NEAR (d, 0.0, eps) << "representable polynomial must leave no detail, level " << l; + } + EXPECT_GT (families, 0u) << "decomposition must produce families to check"; +} + +/* Cell mass (mean * volume) sums to the exact domain integral (c * total + * volume); pins the shape-dependent constant-mode factor via mean_val. */ +TYPED_TEST (mra_projection, constant_mode_reconstructs_domain_mass) +{ + constexpr auto Shape = TypeParam::Shape; + constexpr auto U = TypeParam::U; + constexpr auto P = TypeParam::P; + constexpr auto DIM = TypeParam::DIM; + + const int max_level = (DIM == 3) ? 2 : 3; + constexpr double amplitude = 2.5; + + mra_example example (max_level); + example.init (constant_func (amplitude)); + auto &mra = example.mra; + + std::array mass = {}; + double total_vol = 0.0; + auto *lmi_map = mra.get_lmi_map (); + for (auto l = 0u; l <= static_cast (max_level); ++l) + for (const auto &[lmi, data] : (*lmi_map)[l]) { + total_vol += data.vol; + const auto mean = mra.mean_val (data); + for (auto u = 0u; u < U; ++u) + mass[u] += mean[u] * data.vol; + } + + for (auto u = 0u; u < U; ++u) { + const double expected = amplitude * (u + 1) * total_vol; + EXPECT_NEAR (mass[u], expected, eps) << "component " << u; + } +} + +/* Reconstructed mass is resolution-independent: same total on level L and L+1. */ +TYPED_TEST (mra_projection, projection_conserves_mass_across_levels) +{ + constexpr auto Shape = TypeParam::Shape; + constexpr auto U = TypeParam::U; + constexpr auto P = TypeParam::P; + constexpr auto DIM = TypeParam::DIM; + + const int coarse_level = (DIM == 3) ? 2 : 3; + + auto reconstructed_mass = [] (auto &mra, int max_level) { + std::array mass = {}; + auto *lmi_map = mra.get_lmi_map (); + for (auto l = 0u; l <= static_cast (max_level); ++l) + for (const auto &[lmi, data] : (*lmi_map)[l]) { + const auto mean = mra.mean_val (data); + for (auto u = 0u; u < U; ++u) + mass[u] += mean[u] * data.vol; + } + return mass; + }; + + auto f = poly_func (); + mra_example coarse (coarse_level); + mra_example fine (coarse_level + 1); + coarse.init (f); + fine.init (f); + + const auto mass_coarse = reconstructed_mass (coarse.mra, coarse_level); + const auto mass_fine = reconstructed_mass (fine.mra, coarse_level + 1); + for (auto u = 0u; u < U; ++u) + EXPECT_NEAR (mass_fine[u], mass_coarse[u], eps) << "component " << u; +} + +/* Projection is linear: scaling the data scales every coefficient. */ +TYPED_TEST (mra_projection, projection_is_linear) +{ + constexpr auto Shape = TypeParam::Shape; + constexpr auto U = TypeParam::U; + constexpr auto P = TypeParam::P; + constexpr auto DIM = TypeParam::DIM; + using element_t = typename t8_mra::multiscale::element_t; + + const int max_level = (DIM == 3) ? 2 : 3; + constexpr double scale = 3.0; + + auto base = jump_func (); + + mra_example plain (max_level); + mra_example scaled (max_level); + plain.init (base); + scaled.init ([base] (auto... x) { + auto v = base (x...); + for (auto &e : v) + e *= scale; + return v; + }); + + auto *plain_map = plain->get_lmi_map (); + auto *scaled_map = scaled->get_lmi_map (); + for (auto l = 0u; l <= static_cast (max_level); ++l) + for (const auto &[lmi, data] : (*plain_map)[l]) { + ASSERT_TRUE (scaled_map->contains (lmi)) << "both grids must be identical, level " << l; + const auto &other = scaled_map->get (lmi); + for (auto u = 0u; u < U; ++u) + for (auto i = 0u; i < element_t::DOF; ++i) { + const auto idx = element_t::dg_idx (u, i); + EXPECT_NEAR (other.u_coeffs[idx], scale * data.u_coeffs[idx], eps) + << "coeff " << i << " component " << u << " level " << l; + } + } +} + +/* Degree <= P-1 polynomial reconstructs exactly at every leaf vertex and + * centroid. */ +TYPED_TEST (mra_projection, projection_reconstructs_field_pointwise) +{ + constexpr auto Shape = TypeParam::Shape; + constexpr auto U = TypeParam::U; + constexpr auto P = TypeParam::P; + constexpr auto DIM = TypeParam::DIM; + + const int max_level = (DIM == 3) ? 2 : 3; + const int num_vertices = t8_eclass_num_vertices[Shape]; + + auto f = poly_func (); + mra_example example (max_level); + example.init (f); + auto &mra = example.mra; + + auto *forest = mra.get_forest (); + auto *user_data = mra.get_user_data (); + auto *lmi_map = mra.get_lmi_map (); + + std::size_t checked = 0; + mra.for_each_local_leaf ( + [&] (t8_locidx_t tree_idx, const t8_element_t *element, unsigned int local_idx, t8_gloidx_t) { + const auto lmi = t8_mra::get_lmi_from_forest_data (user_data, local_idx); + const auto &data = lmi_map->get (lmi); + + for (const auto &point : leaf_sample_points (forest, tree_idx, element, num_vertices)) { + const auto got = mra.evaluate (tree_idx, element, data, point); + const auto exact = eval_func (f, point); + for (auto u = 0u; u < U; ++u) + EXPECT_NEAR (got[u], exact[u], eps) << "reconstruction must match, component " << u; + } + ++checked; + }); + EXPECT_GT (checked, 0u) << "must reconstruct at least one leaf"; +} + +/* Triangle projection is independent of the derived vertex order: a refined grid + * carries several orders, each reconstructs the polynomial exactly. */ +TYPED_TEST (mra_projection, triangle_projection_is_vertex_order_invariant) +{ + constexpr auto Shape = TypeParam::Shape; + constexpr auto U = TypeParam::U; + constexpr auto P = TypeParam::P; + constexpr auto DIM = TypeParam::DIM; + + if constexpr (Shape != T8_ECLASS_TRIANGLE) { + GTEST_SKIP () << "vertex ordering is triangle-specific"; + } + else { + const int max_level = 3; + + auto f = poly_func (); + mra_example example (max_level); + example.init (f); + auto &mra = example.mra; + + auto *forest = mra.get_forest (); + auto *user_data = mra.get_user_data (); + auto *lmi_map = mra.get_lmi_map (); + const int num_vertices = t8_eclass_num_vertices[Shape]; + + std::set> orders; + mra.for_each_local_leaf ( + [&] (t8_locidx_t tree_idx, const t8_element_t *element, unsigned int local_idx, t8_gloidx_t) { + const auto lmi = t8_mra::get_lmi_from_forest_data (user_data, local_idx); + const auto &data = lmi_map->get (lmi); + orders.insert (data.order); + + for (const auto &point : leaf_sample_points (forest, tree_idx, element, num_vertices)) { + const auto got = mra.evaluate (tree_idx, element, data, point); + const auto exact = eval_func (f, point); + for (auto u = 0u; u < U; ++u) + EXPECT_NEAR (got[u], exact[u], eps) << "reconstruction must match under every order, component " << u; + } + }); + EXPECT_GT (orders.size (), 1u) << "the refined grid must exercise more than one vertex order"; + } +} + +/* evaluate_point finds the owning leaf: exact at interior points, nullopt + * outside the domain. */ +TYPED_TEST (mra_projection, evaluate_point_reconstructs_field) +{ + constexpr auto Shape = TypeParam::Shape; + constexpr auto U = TypeParam::U; + constexpr auto P = TypeParam::P; + constexpr auto DIM = TypeParam::DIM; + + const int max_level = (DIM == 3) ? 2 : 3; + + auto f = poly_func (); + mra_example example (max_level); + example.init (f); + auto &mra = example.mra; + + std::vector> points; + if constexpr (DIM == 1) + points = { { 0.3 }, { 0.6 }, { 0.15 } }; + else if constexpr (DIM == 2) + points = { { 0.3, 0.2 }, { 0.15, 0.8 }, { 0.7, 0.25 } }; + else + points = { { 0.3, 0.2, 0.4 }, { 0.15, 0.6, 0.7 }, { 0.6, 0.25, 0.1 } }; + + for (const auto &point : points) { + const auto got = mra.evaluate_point (point); + ASSERT_TRUE (got.has_value ()) << "a point inside the domain must be owned by a leaf"; + const auto exact = eval_func (f, point); + for (auto u = 0u; u < U; ++u) + EXPECT_NEAR ((*got)[u], exact[u], eps) << "reconstruction must match, component " << u; + } + + std::array outside; + outside.fill (0.5); + outside[0] = 1.5; + EXPECT_FALSE (mra.evaluate_point (outside).has_value ()) << "a point outside the domain must not be owned"; +} + +/* mean_val returns the cell average: for constant data it is the amplitude on + * every leaf. */ +TYPED_TEST (mra_projection, mean_val_equals_constant_data) +{ + constexpr auto Shape = TypeParam::Shape; + constexpr auto U = TypeParam::U; + constexpr auto P = TypeParam::P; + constexpr auto DIM = TypeParam::DIM; + + const int max_level = (DIM == 3) ? 2 : 3; + constexpr double amplitude = 2.5; + + mra_example example (max_level); + example.init (constant_func (amplitude)); + auto &mra = example.mra; + + auto *lmi_map = mra.get_lmi_map (); + for (auto l = 0u; l <= static_cast (max_level); ++l) + for (const auto &[lmi, data] : (*lmi_map)[l]) { + const auto mean = mra.mean_val (data); + for (auto u = 0u; u < U; ++u) + EXPECT_NEAR (mean[u], amplitude * (u + 1), eps) << "component " << u; + } +} + +/* evaluate_gradient reproduces the exact, constant gradient of a linear field + * (representable for P >= 2). */ +TYPED_TEST (mra_projection, gradient_matches_linear_field) +{ + constexpr auto Shape = TypeParam::Shape; + constexpr auto U = TypeParam::U; + constexpr auto P = TypeParam::P; + constexpr auto DIM = TypeParam::DIM; + + if constexpr (P == 1) { + GTEST_SKIP () << "a linear field is not representable at P = 1"; + } + else { + const int max_level = (DIM == 3) ? 2 : 3; + const int num_vertices = t8_eclass_num_vertices[Shape]; + + std::array slope; + if constexpr (DIM == 1) + slope = { 0.3 }; + else if constexpr (DIM == 2) + slope = { 0.3, 0.4 }; + else + slope = { 0.3, 0.4, 0.2 }; + + auto f = [slope] (auto... coords) { + const std::array x { coords... }; + std::array res = {}; + for (auto u = 0u; u < U; ++u) { + double v = 0.0; + for (auto d = 0u; d < DIM; ++d) + v += slope[d] * x[d]; + res[u] = (u + 1) * v; + } + return res; + }; + + mra_example example (max_level); + example.init (f); + auto &mra = example.mra; + + auto *forest = mra.get_forest (); + auto *user_data = mra.get_user_data (); + auto *lmi_map = mra.get_lmi_map (); + + std::size_t checked = 0; + mra.for_each_local_leaf ( + [&] (t8_locidx_t tree_idx, const t8_element_t *element, unsigned int local_idx, t8_gloidx_t) { + const auto lmi = t8_mra::get_lmi_from_forest_data (user_data, local_idx); + const auto &data = lmi_map->get (lmi); + const auto centroid = leaf_sample_points (forest, tree_idx, element, num_vertices).back (); + + const auto grad = mra.evaluate_gradient (tree_idx, element, data, centroid); + for (auto u = 0u; u < U; ++u) + for (auto dir = 0u; dir < DIM; ++dir) + EXPECT_NEAR (grad[u][dir], (u + 1) * slope[dir], eps) << "gradient component " << u << " dir " << dir; + ++checked; + }); + EXPECT_GT (checked, 0u) << "must check at least one leaf"; + } +} + +} // namespace + +#endif /* T8_ENABLE_MRA */ diff --git a/test/t8_types/t8_gtest_type.cxx b/test/t8_types/t8_gtest_type.cxx index 12bc836dc3..6c0e187e4d 100644 --- a/test/t8_types/t8_gtest_type.cxx +++ b/test/t8_types/t8_gtest_type.cxx @@ -28,6 +28,7 @@ along with t8code; if not, write to the Free Software Foundation, Inc., #include #include #include +#include /* Tags to difference between strong types */ struct dummy_int diff --git a/tutorials/CMakeLists.txt b/tutorials/CMakeLists.txt index 4c21e232db..604d05a74e 100644 --- a/tutorials/CMakeLists.txt +++ b/tutorials/CMakeLists.txt @@ -75,7 +75,10 @@ add_t8_tutorial( NAME t8_step7_interpolation SOURCES general/t8_step7 add_t8_tutorial( NAME t8_tutorial_build_cmesh SOURCES general/t8_tutorial_build_cmesh.cxx general/t8_tutorial_build_cmesh_main.cxx) add_t8_tutorial( NAME t8_tutorial_search SOURCES general/t8_tutorial_search.cxx general/t8_step3_adapt_forest.cxx ) add_t8_tutorial( NAME t8_features_curved_meshes SOURCES features/t8_features_curved_meshes.cxx) -add_t8_tutorial( NAME t8_tutorial_build_scheme SOURCES general/t8_tutorial_build_scheme.cxx) +add_t8_tutorial( NAME t8_tutorial_build_scheme SOURCES general/t8_tutorial_build_scheme.cxx) +add_t8_tutorial( NAME t8_mra_example SOURCES general/t8_mra_example.cxx ) +add_t8_tutorial( NAME t8_mra_dataset SOURCES general/t8_mra_dataset.cxx ) +add_t8_tutorial( NAME t8_mra_nodal SOURCES general/t8_mra_nodal.cxx ) copy_tutorial_file (features/t8_features_curved_meshes_generate_cmesh_hex.geo) copy_tutorial_file (features/t8_features_curved_meshes_generate_cmesh_quad.geo) copy_tutorial_file (features/t8_features_curved_meshes_generate_cmesh_tet.geo) diff --git a/tutorials/general/t8_mra_dataset.cxx b/tutorials/general/t8_mra_dataset.cxx new file mode 100644 index 0000000000..e638239432 --- /dev/null +++ b/tutorials/general/t8_mra_dataset.cxx @@ -0,0 +1,116 @@ +/** + * @file t8_mra_dataset.cxx + * @brief MRA tutorial: adapt to a given structured dataset + * + * A structured_dataset is a func-compatible sampler, so a grid of samples + * drops straight into initialize_data_adaptive: the MRA builds an adaptive grid + * that resolves the dataset's features without ever building the uniform grid. + * Here the source samples come from an analytic field, but any structured field + * (measurement, simulation output, image) works the same way. + */ + +#include "t8.h" +#ifdef T8_ENABLE_MRA + +#include "t8_mra/t8_mra.hxx" +#include "t8_cmesh/t8_cmesh.h" +#include "t8_cmesh/t8_cmesh_examples.h" + +#include +#include +#include +#include +#include + +static void +root_print (const std::string &s) +{ + int rank; + sc_MPI_Comm_rank (sc_MPI_COMM_WORLD, &rank); + if (rank == 0) + std::cout << s; +} + +/// Sample an analytic field on an N x N grid over [0,1]^2 to build a dataset. +/// Value layout matches structured_dataset: axis 0 (x) slowest, axis 1 (y) +/// fastest, one component per node. +static t8_mra::structured_dataset<2, 1> +build_source_dataset (std::size_t n) +{ + std::vector values (n * n); + const double h = 1.0 / static_cast (n - 1); + + for (std::size_t i = 0; i < n; ++i) + for (std::size_t j = 0; j < n; ++j) { + const double x = i * h; + const double y = j * h; + const double r = x * x + y * y; + values[i * n + j] = (r < 0.25) ? (x * y + x + 3.0) : (x * x * y - 2.0 * x * y * y + 3.0 * x); + } + + return t8_mra::structured_dataset<2, 1> ({ n, n }, { 0.0, 0.0 }, { h, h }, std::move (values)); +} + +void +example_dataset () +{ + root_print ("\n=== MRA: adaptive grid from a structured dataset ===\n"); + + constexpr int U = 1; + constexpr int P = 3; + const int max_level = 7; + const double c_thresh = 1.0; + const std::size_t source_nodes = 129; // 2^7 + 1: source resolves the level-7 grid + + const auto dataset = build_source_dataset (source_nodes); + + t8_mra::multiscale mra (max_level, sc_MPI_COMM_WORLD); + + t8_cmesh_t cmesh = t8_cmesh_new_hypercube (T8_ECLASS_QUAD, sc_MPI_COMM_WORLD, 0, 0, 0); + auto *scheme = t8_scheme_new_default (); + t8_cmesh_ref (cmesh); + t8_scheme_ref (const_cast (scheme)); + + mra.initialize_data_adaptive (cmesh, scheme, max_level, dataset, t8_mra::hard_thresholding { .c_thresh = c_thresh }); + mra.balance (); + + const auto num_adaptive = t8_forest_get_global_num_leaf_elements (mra.get_forest ()); + const auto num_uniform = static_cast (std::pow (4, max_level)); + root_print (" Adaptive grid: " + std::to_string (num_adaptive) + " elements (uniform level " + + std::to_string (max_level) + ": " + std::to_string (num_uniform) + ")\n"); + + t8_mra::write_forest_lagrange_vtk (mra, "mra_output/dataset_adaptive", P - 1); + root_print (" Wrote mra_output/dataset_adaptive (open in ParaView)\n"); + + mra.cleanup (); + t8_cmesh_destroy (&cmesh); + t8_scheme_unref (const_cast (&scheme)); +} + +int +main (int argc, char **argv) +{ + int mpiret = sc_MPI_Init (&argc, &argv); + SC_CHECK_MPI (mpiret); + + sc_init (sc_MPI_COMM_WORLD, 1, 1, nullptr, SC_LP_ESSENTIAL); + t8_init (SC_LP_PRODUCTION); + + int mpirank; + sc_MPI_Comm_rank (sc_MPI_COMM_WORLD, &mpirank); + if (mpirank == 0) + std::filesystem::create_directory ("mra_output"); + sc_MPI_Barrier (sc_MPI_COMM_WORLD); + + example_dataset (); + + root_print ("\nDone. Output in mra_output/.\n"); + + sc_finalize (); + + mpiret = sc_MPI_Finalize (); + SC_CHECK_MPI (mpiret); + return 0; +} + +#endif // T8_ENABLE_MRA diff --git a/tutorials/general/t8_mra_example.cxx b/tutorials/general/t8_mra_example.cxx new file mode 100644 index 0000000000..3671e2ea00 --- /dev/null +++ b/tutorials/general/t8_mra_example.cxx @@ -0,0 +1,524 @@ +/** + * @file t8_mra_example.cxx + * @brief MRA tutorial: adaptive coarsening/refinement with VTK output + * + * Demonstrates: + * 1. Full adaptation cycle (top-down): initialize -> coarsen -> refine -> coarsen + * 2. Bottom-up initialization: adaptive grid without building the uniform grid + * 3. Custom adaptation criteria + * 4. Triangle vs quad comparison on the same data + * 5. 3D (hex) adaptation + * 6. Two state variables (U = 2) with different jump locations + */ + +#include "t8.h" +#ifdef T8_ENABLE_MRA + +#include "t8_mra/t8_mra.hxx" +#include "t8_cmesh/t8_cmesh.h" +#include "t8_cmesh/t8_cmesh_examples.h" + +#include +#include +#include +#include + +//============================================================================= +// Output Helpers +//============================================================================= + +/** + * @brief Rank-0-only stdout + * + * Every printed value in the examples is global (global element counts, + * parameters), so the other ranks would only duplicate the lines. + */ +struct root_ostream +{ + bool root; + + template + const root_ostream & + operator<< (const T &v) const + { + if (root) + std::cout << v; + return *this; + } +}; + +static root_ostream +root_out () +{ + int rank; + sc_MPI_Comm_rank (sc_MPI_COMM_WORLD, &rank); + return { rank == 0 }; +} + +//============================================================================= +// Test Functions +//============================================================================= + +/** + * @brief Gaussian bump function + */ +template +auto +gaussian_bump () +{ + return [] (double x, double y) -> std::array { + const double cx = 0.5, cy = 0.5; + const double sigma = 0.15; + const double r2 = (x - cx) * (x - cx) + (y - cy) * (y - cy); + return { std::exp (-r2 / (2 * sigma * sigma)) }; + }; +} + +/** + * @brief Sine wave function + */ +template +auto +sine_wave () +{ + return + [] (double x, double y) -> std::array { return { std::sin (4 * M_PI * x) * std::sin (4 * M_PI * y) }; }; +} + +/** + * @brief Step function (discontinuous) + */ +template +auto +step_function () +{ + return [] (double x, double y) -> std::array { return { (x > 0.5 && y > 0.5) ? 1.0 : 0.0 }; }; +} + +/** + * @brief Two polynomials separated by a jump along a quarter circle + */ +template +auto +quarter_circle () +{ + return [] (double x, double y) -> std::array { + double r = x * x + y * y; + return { (r < 0.25) ? (x * y + x + 3.) : (x * x * y - 2. * x * y * y + 3. * x) }; + }; +} + +/** + * @brief Two components with quarter-circle jumps around the bottom-left + * (u0) and top-right (u1) corner + */ +template +auto +two_quarter_circles () +{ + return [] (double x, double y) -> std::array { + const double xm = 1. - x; + const double ym = 1. - y; + const double r0 = x * x + y * y; + const double r1 = xm * xm + ym * ym; + return { (r0 < 0.25) ? (x * y + x + 3.) : (x * x * y - 2. * x * y * y + 3. * x), + (r1 < 0.25) ? (xm * ym + xm + 3.) : (xm * xm * ym - 2. * xm * ym * ym + 3. * xm) }; + }; +} + +/** + * @brief 3D Gaussian bump function + */ +template +auto +gaussian_bump_3d () +{ + return [] (double x, double y, double z) -> std::array { + const double cx = 0.5, cy = 0.5, cz = 0.5; + const double sigma = 0.15; + const double r2 = (x - cx) * (x - cx) + (y - cy) * (y - cy) + (z - cz) * (z - cz); + return { std::exp (-r2 / (2 * sigma * sigma)) }; + }; +} + +//============================================================================= +// Helpers +//============================================================================= + +/** + * @brief Write high-order Lagrange VTK output (polynomial degree P-1) + */ +template +void +write_vtk_output (MRA &mra, const std::string &filename) +{ + root_out () << " Writing VTK: " << filename << ".vtu\n"; + + t8_mra::write_forest_lagrange_vtk (mra, filename.c_str (), MRA::P_DIM - 1); +} + +/** + * @brief Print element and DOF count of the current grid + */ +template +t8_gloidx_t +print_grid_stats (MRA &mra, const std::string &label) +{ + const auto num_elements = t8_forest_get_global_num_leaf_elements (mra.get_forest ()); + root_out () << " " << label << ": " << num_elements << " elements, " << (num_elements * MRA::DOF * MRA::U_DIM) + << " DOF\n"; + return num_elements; +} + +//============================================================================= +// Example 1: Full Adaptation Cycle (top-down) +//============================================================================= + +/** + * Initialize on the uniform max_level grid, then coarsen away the + * non-significant details, refine via Harten's prediction (grading band at + * the jump) and coarsen again: the zero-detail children created by the + * refinement carry no information, so the grid returns to the coarsened one. + */ +void +example_adaptation_cycle () +{ + root_out () << "\n=== 1. Triangle: full adaptation cycle (top-down) ===\n"; + + constexpr int U = 1; + constexpr int P = 3; + const int min_level = 0; + const int max_level = 7; + const double c_thresh = 1.0; + + t8_mra::multiscale mra (max_level, sc_MPI_COMM_WORLD); + + t8_cmesh_t cmesh = t8_cmesh_new_hypercube (T8_ECLASS_TRIANGLE, sc_MPI_COMM_WORLD, 0, 0, 0); + auto *scheme = t8_scheme_new_default (); + /* The forest takes ownership of cmesh and scheme; keep our own references + * since we destroy/unref them explicitly below. */ + t8_cmesh_ref (cmesh); + t8_scheme_ref (const_cast (scheme)); + + mra.initialize_data (cmesh, scheme, max_level, quarter_circle ()); + print_grid_stats (mra, "Uniform level " + std::to_string (max_level)); + write_vtk_output (mra, "mra_output/01_cycle_step0_uniform"); + + mra.coarsen (min_level, max_level, t8_mra::hard_thresholding { .c_thresh = c_thresh }); + const auto num_coarse = print_grid_stats (mra, "After coarsening"); + write_vtk_output (mra, "mra_output/01_cycle_step1_coarsened"); + + mra.refine (min_level, max_level, t8_mra::harten_prediction { .c_thresh = c_thresh }); + print_grid_stats (mra, "After refinement"); + write_vtk_output (mra, "mra_output/01_cycle_step2_refined"); + + mra.coarsen (min_level, max_level, t8_mra::hard_thresholding { .c_thresh = c_thresh }); + const auto num_recoarse = print_grid_stats (mra, "After second coarsening"); + write_vtk_output (mra, "mra_output/01_cycle_step3_coarsened"); + + root_out () << " Round-trip: " << num_coarse << " -> " << num_recoarse + << (num_coarse == num_recoarse ? " (exact)\n" : "\n"); + + mra.balance (); + const auto num_balanced = print_grid_stats (mra, "After balancing"); + write_vtk_output (mra, "mra_output/01_cycle_step4_balance"); + + mra.cleanup (); + t8_cmesh_destroy (&cmesh); + t8_scheme_unref (const_cast (&scheme)); +} + +//============================================================================= +// Example 2: Bottom-Up Initialization +//============================================================================= + +/** + * Build the adaptive grid directly from the initial data: project on level 1, + * then refine level by level only where the details are significant. The + * uniform max_level grid of example 1 is never built. + */ +void +example_bottom_up () +{ + root_out () << "\n=== 2. Triangle: bottom-up initialization ===\n"; + + constexpr int U = 1; + constexpr int P = 3; + const int max_level = 7; + const double c_thresh = 1.0; + + t8_mra::multiscale mra (max_level, sc_MPI_COMM_WORLD); + + t8_cmesh_t cmesh = t8_cmesh_new_hypercube (T8_ECLASS_TRIANGLE, sc_MPI_COMM_WORLD, 0, 0, 0); + auto *scheme = t8_scheme_new_default (); + t8_cmesh_ref (cmesh); + t8_scheme_ref (const_cast (scheme)); + + mra.initialize_data_adaptive (cmesh, scheme, max_level, quarter_circle (), + t8_mra::hard_thresholding { .c_thresh = c_thresh }); + + const auto num_adaptive = print_grid_stats (mra, "Adaptive grid"); + const auto num_trees = t8_forest_get_num_global_trees (mra.get_forest ()); + const auto num_uniform = num_trees * static_cast (std::pow (4, max_level)); + root_out () << " Uniform level " << max_level << " grid (never built): " << num_uniform << " elements\n"; + root_out () << " Compression: " << (100.0 * (1.0 - static_cast (num_adaptive) / num_uniform)) << " %\n"; + + write_vtk_output (mra, "mra_output/02_bottom_up"); + + mra.cleanup (); + t8_cmesh_destroy (&cmesh); + t8_scheme_unref (const_cast (&scheme)); +} + +//============================================================================= +// Example 3: Custom Adaptation Criterion +//============================================================================= + +/** + * @brief Hard thresholding with an enforced minimum refinement level + * + * Any type satisfying the coarsening_criterion concept can be passed to + * coarsen(). This one composes the default thresholding with a level floor: + * families below floor_level are always kept refined. + */ +struct thresholding_with_floor +{ + t8_mra::hard_thresholding thresholding; + unsigned int floor_level = 4; + + template + void + prepare (MRA &mra) + { + thresholding.prepare (mra); + } + + template + bool + significant (MRA &mra, const typename MRA::levelmultiindex &lmi) + { + return lmi.level () < floor_level || thresholding.significant (mra, lmi); + } +}; + +void +example_custom_criterion () +{ + root_out () << "\n=== 3. Quad: custom coarsening criterion (level floor) ===\n"; + + constexpr int U = 1; + constexpr int P = 3; + const int min_level = 0; + const int max_level = 6; + const double c_thresh = 1.0; + const unsigned int floor_level = 4; + + t8_mra::multiscale mra_plain (max_level, sc_MPI_COMM_WORLD); + t8_mra::multiscale mra_floor (max_level, sc_MPI_COMM_WORLD); + + t8_cmesh_t cmesh = t8_cmesh_new_hypercube (T8_ECLASS_QUAD, sc_MPI_COMM_WORLD, 0, 0, 0); + auto *scheme = t8_scheme_new_default (); + /* Two forests consume one reference each; keep our own on top */ + t8_cmesh_ref (cmesh); + t8_cmesh_ref (cmesh); + t8_scheme_ref (const_cast (scheme)); + t8_scheme_ref (const_cast (scheme)); + + auto func = gaussian_bump (); + + mra_plain.initialize_data (cmesh, scheme, max_level, func); + mra_plain.coarsen (min_level, max_level, t8_mra::hard_thresholding { .c_thresh = c_thresh }); + print_grid_stats (mra_plain, "Plain thresholding"); + write_vtk_output (mra_plain, "mra_output/03_criterion_plain"); + + mra_floor.initialize_data (cmesh, scheme, max_level, func); + mra_floor.coarsen (min_level, max_level, thresholding_with_floor { { .c_thresh = c_thresh }, floor_level }); + print_grid_stats (mra_floor, "With level floor " + std::to_string (floor_level)); + write_vtk_output (mra_floor, "mra_output/03_criterion_floor"); + + mra_plain.cleanup (); + mra_floor.cleanup (); + t8_cmesh_destroy (&cmesh); + t8_scheme_unref (const_cast (&scheme)); +} + +//============================================================================= +// Example 4: Triangle vs Quad +//============================================================================= + +template +t8_gloidx_t +run_shape (const std::string &name, auto &&func, int max_level, double c_thresh) +{ + constexpr int U = 1; + constexpr int P = 3; + + t8_mra::multiscale mra (max_level, sc_MPI_COMM_WORLD); + + t8_cmesh_t cmesh = t8_cmesh_new_hypercube (Shape, sc_MPI_COMM_WORLD, 0, 0, 0); + auto *scheme = t8_scheme_new_default (); + t8_cmesh_ref (cmesh); + t8_scheme_ref (const_cast (scheme)); + + mra.initialize_data (cmesh, scheme, max_level, func); + const auto num_uniform = t8_forest_get_global_num_leaf_elements (mra.get_forest ()); + + mra.coarsen (0, max_level, t8_mra::hard_thresholding { .c_thresh = c_thresh }); + + const auto num_adapted = print_grid_stats (mra, name + " adapted"); + root_out () << " " << num_uniform << " -> " << num_adapted + << " elements (compression: " << (100.0 * (1.0 - static_cast (num_adapted) / num_uniform)) + << " %)\n"; + write_vtk_output (mra, "mra_output/04_compare_" + name); + + mra.cleanup (); + t8_cmesh_destroy (&cmesh); + t8_scheme_unref (const_cast (&scheme)); + + return num_adapted; +} + +/** + * Same data, same threshold, same domain: compare how triangle and quad + * grids adapt. The triangle hypercube consists of two base trees, the quad + * of one; DOF per element differ (P(P+1)/2 vs P^2), so compare total DOF. + */ +void +example_triangle_vs_quad () +{ + root_out () << "\n=== 4. Triangle vs quad on the same data ===\n"; + + const int max_level = 6; + const double c_thresh = 1.0; + auto func = quarter_circle<1> (); + + run_shape ("triangle", func, max_level, c_thresh); + run_shape ("quad", func, max_level, c_thresh); +} + +//============================================================================= +// Example 5: Hex (3D) +//============================================================================= + +void +example_hex_3d () +{ + root_out () << "\n=== 5. Hex: 3D adaptation ===\n"; + + constexpr int U = 1; + constexpr int P = 3; + const int min_level = 0; + const int max_level = 4; + const double c_thresh = 1.0; + + t8_mra::multiscale mra (max_level, sc_MPI_COMM_WORLD); + + t8_cmesh_t cmesh = t8_cmesh_new_hypercube (T8_ECLASS_HEX, sc_MPI_COMM_WORLD, 0, 0, 0); + auto *scheme = t8_scheme_new_default (); + t8_cmesh_ref (cmesh); + t8_scheme_ref (const_cast (scheme)); + + mra.initialize_data (cmesh, scheme, max_level, gaussian_bump_3d ()); + print_grid_stats (mra, "Uniform level " + std::to_string (max_level)); + write_vtk_output (mra, "mra_output/05_hex_step0_uniform"); + + mra.coarsen (min_level, max_level, t8_mra::hard_thresholding { .c_thresh = c_thresh }); + print_grid_stats (mra, "After coarsening"); + write_vtk_output (mra, "mra_output/05_hex_step1_coarsened"); + + mra.refine (min_level, max_level, t8_mra::harten_prediction { .c_thresh = c_thresh }); + print_grid_stats (mra, "After refinement"); + write_vtk_output (mra, "mra_output/05_hex_step2_refined"); + + mra.cleanup (); + t8_cmesh_destroy (&cmesh); + t8_scheme_unref (const_cast (&scheme)); + + root_out () << " Use ParaView 'Clip' / 'Slice' filters to see the internal structure.\n"; +} + +//============================================================================= +// Example 6: Two State Variables +//============================================================================= + +/** + * U = 2: each component carries its own quarter-circle jump (bottom-left vs + * top-right corner). Significance is the maximum over the components, so + * the grid refines along both arcs. + */ +void +example_two_components () +{ + root_out () << "\n=== 6. Triangle: two state variables ===\n"; + + constexpr int U = 2; + constexpr int P = 3; + const int min_level = 0; + const int max_level = 7; + const double c_thresh = 0.2; + + t8_mra::multiscale mra (max_level, sc_MPI_COMM_WORLD); + + t8_cmesh_t cmesh = t8_cmesh_new_hypercube (T8_ECLASS_TRIANGLE, sc_MPI_COMM_WORLD, 0, 0, 0); + auto *scheme = t8_scheme_new_default (); + t8_cmesh_ref (cmesh); + t8_scheme_ref (const_cast (scheme)); + + mra.initialize_data_adaptive (cmesh, scheme, max_level, two_quarter_circles ()); + print_grid_stats (mra, "Uniform level " + std::to_string (max_level)); + write_vtk_output (mra, "mra_output/06_two_components_step0_initial"); + + mra.coarsen (min_level, max_level, t8_mra::hard_thresholding { .c_thresh = c_thresh }); + print_grid_stats (mra, "After coarsening"); + write_vtk_output (mra, "mra_output/06_two_components_step1_coarsened"); + + mra.refine (min_level, max_level, t8_mra::harten_prediction { .c_thresh = c_thresh }); + print_grid_stats (mra, "After refinement"); + write_vtk_output (mra, "mra_output/06_two_components_step2_refined"); + + root_out () << " Color by u0 / u1 in ParaView: the grid follows both jumps.\n"; + + mra.cleanup (); + t8_cmesh_destroy (&cmesh); + t8_scheme_unref (const_cast (&scheme)); +} + +//============================================================================= +// Main +//============================================================================= + +int +main (int argc, char **argv) +{ + int mpiret = sc_MPI_Init (&argc, &argv); + SC_CHECK_MPI (mpiret); + + sc_init (sc_MPI_COMM_WORLD, 1, 1, nullptr, SC_LP_ESSENTIAL); + t8_init (SC_LP_PRODUCTION); + + // Rank 0 creates the output directory; everyone writes into it + int mpirank; + sc_MPI_Comm_rank (sc_MPI_COMM_WORLD, &mpirank); + if (mpirank == 0) + std::filesystem::create_directory ("mra_output"); + sc_MPI_Barrier (sc_MPI_COMM_WORLD); + if (!std::filesystem::exists ("mra_output")) + t8_errorf ("Could not create directory"); + + example_adaptation_cycle (); + example_bottom_up (); + example_custom_criterion (); + example_triangle_vs_quad (); + example_hex_3d (); + example_two_components (); + + root_out () << "\nAll examples completed. Output in mra_output/ (open in ParaView).\n"; + + sc_finalize (); + + mpiret = sc_MPI_Finalize (); + SC_CHECK_MPI (mpiret); + return 0; +} + +#endif // T8_ENABLE_MRA diff --git a/tutorials/general/t8_mra_nodal.cxx b/tutorials/general/t8_mra_nodal.cxx new file mode 100644 index 0000000000..16d6015ce5 --- /dev/null +++ b/tutorials/general/t8_mra_nodal.cxx @@ -0,0 +1,255 @@ +/** + * @file t8_mra_nodal.cxx + * @brief MRA tutorial: adapt a nodal DG forest and get a nodal forest back + * + * A nodal DG code stores its solution as values at nodal points per cell; the + * MRA stores modal coefficients. This tutorial takes a forest already carrying + * the caller's nodal data (here two components), runs the MRA grid adaptation + * on it, and returns a new forest on the adapted grid carrying the + * reconstructed nodal data in the caller's own per-cell layout (attached as + * forest user data). The returned forest is self-contained and outlives the + * multiscale object. + * + * The bridge is two converters: initialize_data_nodal (nodal -> modal on load) + * and export_data_nodal (modal -> nodal on the adapted grid). The nodal + * solution is written to VTK before and after adaptation. + */ + +#include "t8.h" +#ifdef T8_ENABLE_MRA + +#include "t8_mra/t8_mra.hxx" +#include "t8_cmesh/t8_cmesh.h" +#include "t8_cmesh/t8_cmesh_examples.h" +#include "t8_forest/t8_forest.h" +#include "t8_forest/t8_forest_ghost.h" +#include "t8_forest/t8_forest_geometrical.h" + +#include +#include +#include +#include +#include +#include +#include + +constexpr int U = 2; +constexpr int P = 3; +constexpr int DOF = P * P; + +/// The caller's nodal DG cell state: values at the DOF nodes per component, +/// component-major (index u*DOF + j), matching the MRA nodal buffer layout. +struct nodal_cell +{ + std::array values; +}; + +using node_set = std::array, DOF>; + +static void +root_print (const std::string &s) +{ + int rank; + sc_MPI_Comm_rank (sc_MPI_COMM_WORLD, &rank); + if (rank == 0) + std::cout << s; +} + +/// Two-component analytic field the fake nodal solver samples. Both components +/// are smooth over large regions with localized sharp features, so the MRA +/// coarsens the smooth areas and keeps the grid fine only around the features: +/// component 0 a Gaussian bump, component 1 a jump ring on a gentle background. +static std::array +source_field (double x, double y) +{ + const auto rc = std::hypot (x - 0.35, y - 0.35); + const auto bump = std::exp (-120.0 * rc * rc); + const auto ring = (std::hypot (x - 0.7, y - 0.7) < 0.2) ? 1.0 : 0.0; + const auto background = 0.2 * std::sin (M_PI * x) * std::sin (M_PI * y); + return { bump + background, ring + background }; +} + +/// Fake a nodal solution on `forest`: one nodal_cell per leaf (SFC order), +/// sampled from source_field at the node coordinates. +static std::vector +sample_nodal (t8_forest_t forest, const node_set &nodes) +{ + std::vector data; + const auto num_trees = t8_forest_get_num_local_trees (forest); + for (t8_locidx_t tree = 0; tree < num_trees; ++tree) { + const auto num_leaves = t8_forest_get_tree_num_leaf_elements (forest, tree); + for (t8_locidx_t e = 0; e < num_leaves; ++e) { + const auto *element = t8_forest_get_leaf_element_in_tree (forest, tree, e); + double v0[3], v3[3]; // axis-aligned box: vertex 0 min, vertex 3 max + t8_forest_element_coordinate (forest, tree, element, 0, v0); + t8_forest_element_coordinate (forest, tree, element, 3, v3); + + nodal_cell cell; + for (int j = 0; j < DOF; ++j) { + const auto x = v0[0] + nodes[j][0] * (v3[0] - v0[0]); + const auto y = v0[1] + nodes[j][1] * (v3[1] - v0[1]); + const auto f = source_field (x, y); + + for (int u = 0; u < U; ++u) + cell.values[u * DOF + j] = f[u]; + } + + data.push_back (cell); + } + } + + return data; +} + +/// Refine cells the solution actually needs: a tight ball on the bump and a +/// band along the jump ring. Leaves the original nodal forest nonuniform but +/// resolving the features (the loader accepts any committed forest). +static int +refine_near_features (t8_forest_t, t8_forest_t forest_from, t8_locidx_t which_tree, t8_eclass_t tree_class, t8_locidx_t, + const t8_scheme_c *scheme, int, int, t8_element_t *elements[]) +{ + constexpr int feature_level = 7; + if (scheme->element_get_level (tree_class, elements[0]) >= feature_level) + return 0; + + double c[3]; + t8_forest_element_centroid (forest_from, which_tree, elements[0], c); + const bool on_bump = std::hypot (c[0] - 0.35, c[1] - 0.35) < 0.12; + const bool on_ring = std::abs (std::hypot (c[0] - 0.7, c[1] - 0.7) - 0.2) < 0.06; + return (on_bump || on_ring) ? 1 : 0; +} + +/// A nonuniform original forest: a uniform grid refined toward the features. +/// Consumes one ref of cmesh and scheme (like t8_forest_new_uniform). +static t8_forest_t +feature_refined_forest (t8_cmesh_t cmesh, const t8_scheme *scheme, sc_MPI_Comm comm) +{ + t8_forest_t forest = t8_forest_new_uniform (cmesh, scheme, 4, 0, comm); + return t8_forest_new_adapt (forest, refine_near_features, 1, 0, nullptr); +} + +/** + * Run the MRA grid adaptation on a nodal DG forest and return the adapted grid + * carrying the reconstructed nodal data. The returned forest has a face-ghost + * layer, and the attached std::vector is ghost-complete: entries + * [0, num_local) are local leaves, [num_local, num_local + num_ghost) the ghost + * cells (filled by one ghost exchange), so it is ready for a nodal solver. The + * forest owns the vector as user data (caller deletes it and unrefs the + * forest). `forest_in` / `nodal_in` stay owned by the caller. + */ +static t8_forest_t +mra_coarsen_nodal (t8_forest_t forest_in, const std::vector &nodal_in, const node_set &nodes, int min_level, + int max_level) +{ + t8_mra::multiscale mra (max_level, sc_MPI_COMM_WORLD); + + // Load: hand each leaf's nodal values to the MRA (SFC order matches nodal_in). + size_t in_idx = 0; + mra.initialize_data_nodal (forest_in, nodes, [&] (int, const t8_element_t *) { return nodal_in[in_idx++].values; }); + t8_mra::write_forest_lagrange_vtk (mra, "mra_output/nodal_before", P - 1); + + mra.coarsen (min_level, max_level, t8_mra::hard_thresholding { .c_thresh = 0.1 }); + mra.balance (); + t8_mra::write_forest_lagrange_vtk (mra, "mra_output/nodal_after", P - 1); + + // Reconstruct nodal values on the adapted grid (local leaves, SFC order). + t8_forest_t adapted = mra.get_forest (); + const auto num_adapted = t8_forest_get_local_num_leaf_elements (adapted); + auto *nodal_adapted = sc_array_new_count (sizeof (nodal_cell), num_adapted); + auto *adapted_cells = reinterpret_cast (nodal_adapted->array); + size_t leaf = 0; + + mra.export_data_nodal (nodes, [&] (int, const t8_element_t *, std::span nodal) { + std::copy (nodal.begin (), nodal.end (), adapted_cells[leaf++].values.begin ()); + }); + + // Returned forest: the adapted grid load-balanced with a face-ghost layer. + // set_partition takes ownership of `adapted`, so ref it to keep the MRA's. + t8_forest_ref (adapted); + t8_forest_t forest_out; + t8_forest_init (&forest_out); + t8_forest_set_ghost (forest_out, 1, T8_GHOST_FACES); + t8_forest_set_partition (forest_out, adapted, 0); + t8_forest_commit (forest_out); + + const auto num_local = t8_forest_get_local_num_leaf_elements (forest_out); + const auto num_ghost = t8_forest_get_num_ghosts (forest_out); + + // Move the nodal data onto the new partition, then fill ghosts by exchange. + auto *nodal_partitioned = sc_array_new_count (sizeof (nodal_cell), num_local); + t8_forest_partition_data (adapted, forest_out, nodal_adapted, nodal_partitioned); + + auto *nodal_with_ghosts = sc_array_new_count (sizeof (nodal_cell), num_local + num_ghost); + std::copy_n (reinterpret_cast (nodal_partitioned->array), num_local, + reinterpret_cast (nodal_with_ghosts->array)); + t8_forest_ghost_exchange_data (forest_out, nodal_with_ghosts); + + auto *nodal_out = new std::vector (num_local + num_ghost); + std::copy_n (reinterpret_cast (nodal_with_ghosts->array), num_local + num_ghost, nodal_out->data ()); + + sc_array_destroy (nodal_adapted); + sc_array_destroy (nodal_partitioned); + sc_array_destroy (nodal_with_ghosts); + mra.cleanup (); + + t8_forest_set_user_data (forest_out, nodal_out); + + return forest_out; +} + +int +main (int argc, char **argv) +{ + int mpiret = sc_MPI_Init (&argc, &argv); + SC_CHECK_MPI (mpiret); + sc_init (sc_MPI_COMM_WORLD, 1, 1, nullptr, SC_LP_ESSENTIAL); + t8_init (SC_LP_PRODUCTION); + + int mpirank; + sc_MPI_Comm_rank (sc_MPI_COMM_WORLD, &mpirank); + if (mpirank == 0) + std::filesystem::create_directory ("mra_output"); + sc_MPI_Barrier (sc_MPI_COMM_WORLD); + + root_print ("\n=== MRA: adapt a two-component nodal DG forest ===\n"); + + const int max_level = 7; + const int min_level = 0; + + // Reference nodes: tensor of P equispaced points on [0,1]^2 (node j = jy*P+jx). + node_set nodes; + for (int jy = 0; jy < P; ++jy) + for (int jx = 0; jx < P; ++jx) + nodes[jy * P + jx] = { jx / static_cast (P - 1), jy / static_cast (P - 1) }; + + // The caller's nodal DG state: a nonuniform forest plus one nodal_cell per leaf. + t8_cmesh_t cmesh = t8_cmesh_new_hypercube (T8_ECLASS_QUAD, sc_MPI_COMM_WORLD, 0, 0, 0); + auto *scheme = t8_scheme_new_default (); + t8_forest_t forest_in = feature_refined_forest (cmesh, scheme, sc_MPI_COMM_WORLD); + const auto nodal_in = sample_nodal (forest_in, nodes); + root_print (" Input nodal forest: " + std::to_string (nodal_in.size ()) + " local cells (plotted: nodal_before)\n"); + + // Adapt with the MRA; get the coarser grid back with nodal data attached. + t8_forest_t forest_out = mra_coarsen_nodal (forest_in, nodal_in, nodes, min_level, max_level); + auto *nodal_out = static_cast *> (t8_forest_get_user_data (forest_out)); + const auto num_local = t8_forest_get_local_num_leaf_elements (forest_out); + const auto num_ghost = t8_forest_get_num_ghosts (forest_out); + root_print (" Output nodal forest: " + std::to_string (num_local) + " local + " + std::to_string (num_ghost) + + " ghost cells (plotted: nodal_after)\n"); + + t8_forest_unref (&forest_in); + + // forest_out + *nodal_out is now a self-contained nodal DG state ready to hand + // back to the solver. Release it once done. + delete nodal_out; + t8_forest_unref (&forest_out); + + root_print ("\nDone. Output in mra_output/.\n"); + + sc_finalize (); + mpiret = sc_MPI_Finalize (); + SC_CHECK_MPI (mpiret); + return 0; +} + +#endif // T8_ENABLE_MRA