diff --git a/src/mesh/CMakeLists.txt b/src/mesh/CMakeLists.txt
index 7d7caaf31cdf3d791de1915660491939efcb32fd..66ce42ae6f9e490a0b3e6e406934478b0f026f4a 100644
--- a/src/mesh/CMakeLists.txt
+++ b/src/mesh/CMakeLists.txt
@@ -22,10 +22,14 @@ add_library(
   MeshBuilderBase.cpp
   MeshCellZone.cpp
   MeshDataManager.cpp
+  MeshEdgeBoundary.cpp
   MeshFaceBoundary.cpp
+  MeshFlatEdgeBoundary.cpp
   MeshFlatFaceBoundary.cpp
   MeshFlatNodeBoundary.cpp
   MeshRelaxer.cpp
+  MeshLineEdgeBoundary.cpp
+  MeshLineFaceBoundary.cpp
   MeshLineNodeBoundary.cpp
   MeshNodeBoundary.cpp
   MeshRandomizer.cpp
diff --git a/src/mesh/CartesianMeshBuilder.cpp b/src/mesh/CartesianMeshBuilder.cpp
index c0cf86a73886b59e56f417fbf1731c78e00d15d5..9f6ea50cab88390669e3471ad3c70f25e870e390 100644
--- a/src/mesh/CartesianMeshBuilder.cpp
+++ b/src/mesh/CartesianMeshBuilder.cpp
@@ -2,7 +2,6 @@
 
 #include <mesh/Connectivity.hpp>
 #include <mesh/LogicalConnectivityBuilder.hpp>
-#include <mesh/RefId.hpp>
 #include <utils/Array.hpp>
 #include <utils/Messenger.hpp>
 
diff --git a/src/mesh/Connectivity.cpp b/src/mesh/Connectivity.cpp
index 7b81eeb0326e7cd5bc5df2b9c6d45b642dcb5e56..5622294b2af633f38b1cc283eaf0b1576498f0d4 100644
--- a/src/mesh/Connectivity.cpp
+++ b/src/mesh/Connectivity.cpp
@@ -95,6 +95,24 @@ Connectivity<Dimension>::_buildFrom(const ConnectivityDescriptor& descriptor)
     m_edge_number   = WeakEdgeValue<int>(*this, node_number_array);
     m_edge_owner    = WeakEdgeValue<int>(*this, node_owner_array);
     m_edge_is_owned = WeakEdgeValue<bool>(*this, node_is_owned_array);
+
+    // edge and face references are set equal to node references
+    m_ref_edge_list_vector.reserve(descriptor.template refItemListVector<ItemType::node>().size());
+    m_ref_face_list_vector.reserve(descriptor.template refItemListVector<ItemType::node>().size());
+    for (auto ref_node_list : descriptor.template refItemListVector<ItemType::node>()) {
+      const RefId ref_id            = ref_node_list.refId();
+      Array<const NodeId> node_list = ref_node_list.list();
+      Array<EdgeId> edge_list(node_list.size());
+      Array<FaceId> face_list(node_list.size());
+      for (size_t i = 0; i < node_list.size(); ++i) {
+        edge_list[i] = EdgeId::base_type{node_list[i]};
+        face_list[i] = FaceId::base_type{node_list[i]};
+      }
+
+      m_ref_edge_list_vector.emplace_back(RefItemList<ItemType::edge>(ref_id, edge_list, ref_node_list.isBoundary()));
+      m_ref_face_list_vector.emplace_back(RefItemList<ItemType::face>(ref_id, face_list, ref_node_list.isBoundary()));
+    }
+
   } else {
     m_item_to_item_matrix[itemTId(ItemType::face)][itemTId(ItemType::node)] = descriptor.face_to_node_vector;
 
@@ -134,6 +152,19 @@ Connectivity<Dimension>::_buildFrom(const ConnectivityDescriptor& descriptor)
       m_edge_owner    = WeakEdgeValue<int>(*this, face_owner_array);
       m_edge_is_owned = WeakEdgeValue<bool>(*this, face_is_owned_array);
 
+      // edge references are set equal to face references
+      m_ref_edge_list_vector.reserve(descriptor.template refItemListVector<ItemType::face>().size());
+      for (auto ref_face_list : descriptor.template refItemListVector<ItemType::face>()) {
+        const RefId ref_id            = ref_face_list.refId();
+        Array<const FaceId> face_list = ref_face_list.list();
+        Array<EdgeId> edge_list(face_list.size());
+        for (size_t i = 0; i < face_list.size(); ++i) {
+          edge_list[i] = EdgeId::base_type{face_list[i]};
+        }
+
+        m_ref_edge_list_vector.emplace_back(RefItemList<ItemType::edge>(ref_id, edge_list, ref_face_list.isBoundary()));
+      }
+
     } else {
       m_item_to_item_matrix[itemTId(ItemType::edge)][itemTId(ItemType::node)] = descriptor.edge_to_node_vector;
 
@@ -243,7 +274,7 @@ Connectivity<Dimension>::_buildIsBoundaryNode() const
 
 template <ItemType item_type, size_t Dimension>
 inline void
-_printReference(std::ostream& os, const Connectivity<Dimension>& connectivity)
+_printReference(std::ostream& os, const Connectivity<Dimension>& connectivity, std::set<std::string>& already_printed)
 {
   auto count_all_items = [](const auto& item_is_owned) -> size_t {
     using ItemId  = typename std::decay_t<decltype(item_is_owned)>::index_type;
@@ -264,10 +295,20 @@ _printReference(std::ostream& os, const Connectivity<Dimension>& connectivity)
 
   os << "- number of " << itemName(item_type) << "s: " << rang::fgB::yellow
      << count_all_items(connectivity.template isOwned<item_type>()) << rang::style::reset << '\n';
-  os << "  " << rang::fgB::yellow << connectivity.template numberOfRefItemList<item_type>() << rang::style::reset
-     << " references\n";
-  if (connectivity.template numberOfRefItemList<item_type>() > 0) {
-    for (size_t i_ref_item = 0; i_ref_item < connectivity.template numberOfRefItemList<item_type>(); ++i_ref_item) {
+
+  // This is done to avoid printing deduced references on subitems
+  std::vector<size_t> to_print_list;
+  for (size_t i_ref_item = 0; i_ref_item < connectivity.template numberOfRefItemList<item_type>(); ++i_ref_item) {
+    auto ref_item_list = connectivity.template refItemList<item_type>(i_ref_item);
+    if (already_printed.find(ref_item_list.refId().tagName()) == already_printed.end()) {
+      to_print_list.push_back(i_ref_item);
+      already_printed.insert(ref_item_list.refId().tagName());
+    }
+  }
+
+  os << "  " << rang::fgB::yellow << to_print_list.size() << rang::style::reset << " references\n";
+  if (to_print_list.size() > 0) {
+    for (size_t i_ref_item : to_print_list) {
       auto ref_item_list = connectivity.template refItemList<item_type>(i_ref_item);
       os << "  - " << rang::fgB::green << ref_item_list.refId().tagName() << rang::style::reset << " ("
          << rang::fgB::green << ref_item_list.refId().tagNumber() << rang::style::reset << ") number "
@@ -281,15 +322,17 @@ template <size_t Dimension>
 std::ostream&
 Connectivity<Dimension>::_write(std::ostream& os) const
 {
+  std::set<std::string> already_printed;
+
   os << "connectivity of dimension " << Dimension << '\n';
-  _printReference<ItemType::cell>(os, *this);
+  _printReference<ItemType::cell>(os, *this, already_printed);
   if constexpr (Dimension > 1) {
-    _printReference<ItemType::face>(os, *this);
+    _printReference<ItemType::face>(os, *this, already_printed);
   }
   if constexpr (Dimension > 2) {
-    _printReference<ItemType::edge>(os, *this);
+    _printReference<ItemType::edge>(os, *this, already_printed);
   }
-  _printReference<ItemType::node>(os, *this);
+  _printReference<ItemType::node>(os, *this, already_printed);
 
   return os;
 }
diff --git a/src/mesh/ConnectivityDispatcher.cpp b/src/mesh/ConnectivityDispatcher.cpp
index 8c10a5a8fe73e350f1c3d7e84a718344fe4d9cf5..fd5b58c6daaf5fd44c0cb440e7af472b9f9aebbf 100644
--- a/src/mesh/ConnectivityDispatcher.cpp
+++ b/src/mesh/ConnectivityDispatcher.cpp
@@ -435,6 +435,17 @@ ConnectivityDispatcher<Dimension>::_buildItemReferenceList()
 
       Assert(number_of_item_list_sender < parallel::size());
 
+      // sending is boundary property
+      Array<bool> ref_item_list_is_boundary{number_of_item_ref_list_per_proc[sender_rank]};
+      if (parallel::rank() == sender_rank) {
+        for (size_t i_item_ref_list = 0; i_item_ref_list < m_connectivity.template numberOfRefItemList<item_type>();
+             ++i_item_ref_list) {
+          auto item_ref_list                         = m_connectivity.template refItemList<item_type>(i_item_ref_list);
+          ref_item_list_is_boundary[i_item_ref_list] = item_ref_list.isBoundary();
+        }
+      }
+      parallel::broadcast(ref_item_list_is_boundary, sender_rank);
+
       // sending references tags
       Array<RefId::TagNumberType> ref_tag_list{number_of_item_ref_list_per_proc[sender_rank]};
       if (parallel::rank() == sender_rank) {
@@ -553,7 +564,8 @@ ConnectivityDispatcher<Dimension>::_buildItemReferenceList()
 
           Array<const ItemId> item_id_array = convert_to_array(item_id_vector);
 
-          m_new_descriptor.addRefItemList(RefItemList<item_type>(ref_id_list[i_ref], item_id_array));
+          bool is_boundary = ref_item_list_is_boundary[i_ref];
+          m_new_descriptor.addRefItemList(RefItemList<item_type>(ref_id_list[i_ref], item_id_array, is_boundary));
         }
       }
     }
diff --git a/src/mesh/DiamondDualConnectivityBuilder.cpp b/src/mesh/DiamondDualConnectivityBuilder.cpp
index 38e62b737d772d2db7d32a2baa4134c9af94f6e9..1afa7d40413f2f8c9d88a99004a78b094d2705e5 100644
--- a/src/mesh/DiamondDualConnectivityBuilder.cpp
+++ b/src/mesh/DiamondDualConnectivityBuilder.cpp
@@ -230,7 +230,8 @@ DiamondDualConnectivityBuilder::_buildDiamondConnectivityFrom(const IConnectivit
         for (size_t i = 0; i < diamond_node_list.size(); ++i) {
           node_array[i] = diamond_node_list[i];
         }
-        diamond_descriptor.addRefItemList(RefNodeList{primal_ref_node_list.refId(), node_array});
+        diamond_descriptor.addRefItemList(
+          RefNodeList{primal_ref_node_list.refId(), node_array, primal_ref_node_list.isBoundary()});
       }
     }
   }
@@ -283,7 +284,8 @@ DiamondDualConnectivityBuilder::_buildDiamondConnectivityFrom(const IConnectivit
         for (size_t i = 0; i < diamond_face_list.size(); ++i) {
           face_array[i] = diamond_face_list[i];
         }
-        diamond_descriptor.addRefItemList(RefFaceList{primal_ref_face_list.refId(), face_array});
+        diamond_descriptor.addRefItemList(
+          RefFaceList{primal_ref_face_list.refId(), face_array, primal_ref_face_list.isBoundary()});
       }
     }
   }
@@ -347,7 +349,8 @@ DiamondDualConnectivityBuilder::_buildDiamondConnectivityFrom(const IConnectivit
         for (size_t i = 0; i < diamond_edge_list.size(); ++i) {
           edge_array[i] = diamond_edge_list[i];
         }
-        diamond_descriptor.addRefItemList(RefEdgeList{primal_ref_edge_list.refId(), edge_array});
+        diamond_descriptor.addRefItemList(
+          RefEdgeList{primal_ref_edge_list.refId(), edge_array, primal_ref_edge_list.isBoundary()});
       }
     }
   }
diff --git a/src/mesh/Dual1DConnectivityBuilder.cpp b/src/mesh/Dual1DConnectivityBuilder.cpp
index 39dea9ddb6ee6c3c6f26660600765e1d9e779b60..ec0389d33ce17ba32141f7fcea8e251eba08ab70 100644
--- a/src/mesh/Dual1DConnectivityBuilder.cpp
+++ b/src/mesh/Dual1DConnectivityBuilder.cpp
@@ -128,7 +128,8 @@ Dual1DConnectivityBuilder::_buildConnectivityFrom(const IConnectivity& i_primal_
         for (size_t i = 0; i < dual_node_list.size(); ++i) {
           node_array[i] = dual_node_list[i];
         }
-        dual_descriptor.addRefItemList(RefNodeList{primal_ref_node_list.refId(), node_array});
+        dual_descriptor.addRefItemList(
+          RefNodeList{primal_ref_node_list.refId(), node_array, primal_ref_node_list.isBoundary()});
       }
     }
   }
diff --git a/src/mesh/GmshReader.cpp b/src/mesh/GmshReader.cpp
index 15b34e1613586a38385d7a4051103b0713dafef2..f12e6ab3c6b17e3ae31110b91789ba8188b53d7c 100644
--- a/src/mesh/GmshReader.cpp
+++ b/src/mesh/GmshReader.cpp
@@ -55,13 +55,31 @@ GmshConnectivityBuilder<1>::GmshConnectivityBuilder(const GmshReader::GmshData&
     ref_points_map[ref].push_back(point_number);
   }
 
+  Array<size_t> node_nb_cell(descriptor.node_number_vector.size());
+  node_nb_cell.fill(0);
+
+  for (size_t j = 0; j < nb_cells; ++j) {
+    for (int r = 0; r < 2; ++r) {
+      node_nb_cell[descriptor.cell_to_node_vector[j][r]] += 1;
+    }
+  }
+
   for (const auto& ref_point_list : ref_points_map) {
     Array<NodeId> point_list(ref_point_list.second.size());
     for (size_t j = 0; j < ref_point_list.second.size(); ++j) {
       point_list[j] = ref_point_list.second[j];
     }
     const GmshReader::PhysicalRefId& physical_ref_id = gmsh_data.m_physical_ref_map.at(ref_point_list.first);
-    descriptor.addRefItemList(RefNodeList(physical_ref_id.refId(), point_list));
+
+    bool is_boundary = true;
+    for (size_t i_node = 0; i_node < point_list.size(); ++i_node) {
+      if (node_nb_cell[point_list[i_node]] > 1) {
+        is_boundary = false;
+        break;
+      }
+    }
+
+    descriptor.addRefItemList(RefNodeList(physical_ref_id.refId(), point_list, is_boundary));
   }
 
   std::map<unsigned int, std::vector<unsigned int>> ref_cells_map;
@@ -77,7 +95,7 @@ GmshConnectivityBuilder<1>::GmshConnectivityBuilder(const GmshReader::GmshData&
       cell_list[j] = ref_cell_list.second[j];
     }
     const GmshReader::PhysicalRefId& physical_ref_id = gmsh_data.m_physical_ref_map.at(ref_cell_list.first);
-    descriptor.addRefItemList(RefCellList(physical_ref_id.refId(), cell_list));
+    descriptor.addRefItemList(RefCellList(physical_ref_id.refId(), cell_list, false));
   }
 
   descriptor.cell_owner_vector.resize(nb_cells);
@@ -139,7 +157,7 @@ GmshConnectivityBuilder<2>::GmshConnectivityBuilder(const GmshReader::GmshData&
       cell_list[j] = ref_cell_list.second[j];
     }
     const GmshReader::PhysicalRefId& physical_ref_id = gmsh_data.m_physical_ref_map.at(ref_cell_list.first);
-    descriptor.addRefItemList(RefCellList(physical_ref_id.refId(), cell_list));
+    descriptor.addRefItemList(RefCellList(physical_ref_id.refId(), cell_list, false));
   }
 
   ConnectivityBuilderBase::_computeCellFaceAndFaceNodeConnectivities<2>(descriptor);
@@ -196,13 +214,42 @@ GmshConnectivityBuilder<2>::GmshConnectivityBuilder(const GmshReader::GmshData&
     }
   }
 
+  Array<size_t> face_nb_cell(descriptor.face_number_vector.size());
+  face_nb_cell.fill(0);
+
+  for (size_t j = 0; j < descriptor.cell_to_face_vector.size(); ++j) {
+    for (size_t l = 0; l < descriptor.cell_to_face_vector[j].size(); ++l) {
+      face_nb_cell[descriptor.cell_to_face_vector[j][l]] += 1;
+    }
+  }
+
   for (const auto& ref_face_list : ref_faces_map) {
     Array<FaceId> face_list(ref_face_list.second.size());
     for (size_t j = 0; j < ref_face_list.second.size(); ++j) {
       face_list[j] = ref_face_list.second[j];
     }
     const GmshReader::PhysicalRefId& physical_ref_id = gmsh_data.m_physical_ref_map.at(ref_face_list.first);
-    descriptor.addRefItemList(RefFaceList{physical_ref_id.refId(), face_list});
+
+    bool is_boundary = true;
+    for (size_t i_face = 0; i_face < face_list.size(); ++i_face) {
+      if (face_nb_cell[face_list[i_face]] > 1) {
+        is_boundary = false;
+        break;
+      }
+    }
+
+    descriptor.addRefItemList(RefFaceList{physical_ref_id.refId(), face_list, is_boundary});
+  }
+
+  Array<bool> is_boundary_node(descriptor.node_number_vector.size());
+  is_boundary_node.fill(false);
+
+  for (size_t i_face = 0; i_face < face_nb_cell.size(); ++i_face) {
+    if (face_nb_cell[i_face] == 1) {
+      for (size_t node_id : descriptor.face_to_node_vector[i_face]) {
+        is_boundary_node[node_id] = true;
+      }
+    }
   }
 
   std::map<unsigned int, std::vector<unsigned int>> ref_points_map;
@@ -218,7 +265,15 @@ GmshConnectivityBuilder<2>::GmshConnectivityBuilder(const GmshReader::GmshData&
       point_list[j] = ref_point_list.second[j];
     }
     const GmshReader::PhysicalRefId& physical_ref_id = gmsh_data.m_physical_ref_map.at(ref_point_list.first);
-    descriptor.addRefItemList(RefNodeList(physical_ref_id.refId(), point_list));
+
+    bool is_boundary = true;
+    for (size_t i_node = 0; i_node < point_list.size(); ++i_node) {
+      if (not is_boundary_node[point_list[i_node]]) {
+        is_boundary = false;
+      }
+    }
+
+    descriptor.addRefItemList(RefNodeList(physical_ref_id.refId(), point_list, is_boundary));
   }
 
   descriptor.cell_owner_vector.resize(nb_cells);
@@ -317,13 +372,22 @@ GmshConnectivityBuilder<3>::GmshConnectivityBuilder(const GmshReader::GmshData&
       cell_list[j] = ref_cell_list.second[j];
     }
     const GmshReader::PhysicalRefId& physical_ref_id = gmsh_data.m_physical_ref_map.at(ref_cell_list.first);
-    descriptor.addRefItemList(RefCellList(physical_ref_id.refId(), cell_list));
+    descriptor.addRefItemList(RefCellList(physical_ref_id.refId(), cell_list, false));
   }
 
   ConnectivityBuilderBase::_computeCellFaceAndFaceNodeConnectivities<3>(descriptor);
 
   const auto& node_number_vector = descriptor.node_number_vector;
 
+  Array<size_t> face_nb_cell(descriptor.face_number_vector.size());
+  face_nb_cell.fill(0);
+
+  for (size_t j = 0; j < descriptor.cell_to_face_vector.size(); ++j) {
+    for (size_t l = 0; l < descriptor.cell_to_face_vector[j].size(); ++l) {
+      face_nb_cell[descriptor.cell_to_face_vector[j][l]] += 1;
+    }
+  }
+
   {
     using Face                                                                 = ConnectivityFace<3>;
     const std::unordered_map<Face, FaceId, typename Face::Hash> face_to_id_map = [&] {
@@ -417,13 +481,33 @@ GmshConnectivityBuilder<3>::GmshConnectivityBuilder(const GmshReader::GmshData&
         face_list[j] = ref_face_list.second[j];
       }
       const GmshReader::PhysicalRefId& physical_ref_id = gmsh_data.m_physical_ref_map.at(ref_face_list.first);
-      descriptor.addRefItemList(RefFaceList{physical_ref_id.refId(), face_list});
+
+      bool is_boundary = true;
+      for (size_t i_face = 0; i_face < face_list.size(); ++i_face) {
+        if (face_nb_cell[face_list[i_face]] > 1) {
+          is_boundary = false;
+          break;
+        }
+      }
+
+      descriptor.addRefItemList(RefFaceList{physical_ref_id.refId(), face_list, is_boundary});
     }
   }
 
   ConnectivityBuilderBase::_computeFaceEdgeAndEdgeNodeAndCellEdgeConnectivities<3>(descriptor);
 
   {
+    Array<bool> is_boundary_edge(descriptor.edge_number_vector.size());
+    is_boundary_edge.fill(false);
+
+    for (size_t i_face = 0; i_face < face_nb_cell.size(); ++i_face) {
+      if (face_nb_cell[i_face] == 1) {
+        for (size_t node_id : descriptor.face_to_edge_vector[i_face]) {
+          is_boundary_edge[node_id] = true;
+        }
+      }
+    }
+
     using Edge                                                                 = ConnectivityFace<2>;
     const auto& node_number_vector                                             = descriptor.node_number_vector;
     const std::unordered_map<Edge, EdgeId, typename Edge::Hash> edge_to_id_map = [&] {
@@ -482,7 +566,26 @@ GmshConnectivityBuilder<3>::GmshConnectivityBuilder(const GmshReader::GmshData&
         edge_list[j] = ref_edge_list.second[j];
       }
       const GmshReader::PhysicalRefId& physical_ref_id = gmsh_data.m_physical_ref_map.at(ref_edge_list.first);
-      descriptor.addRefItemList(RefEdgeList{physical_ref_id.refId(), edge_list});
+
+      bool is_boundary = true;
+      for (size_t i_node = 0; i_node < edge_list.size(); ++i_node) {
+        if (not is_boundary_edge[edge_list[i_node]]) {
+          is_boundary = false;
+        }
+      }
+
+      descriptor.addRefItemList(RefEdgeList{physical_ref_id.refId(), edge_list, is_boundary});
+    }
+  }
+
+  Array<bool> is_boundary_node(descriptor.node_number_vector.size());
+  is_boundary_node.fill(false);
+
+  for (size_t i_face = 0; i_face < face_nb_cell.size(); ++i_face) {
+    if (face_nb_cell[i_face] == 1) {
+      for (size_t node_id : descriptor.face_to_node_vector[i_face]) {
+        is_boundary_node[node_id] = true;
+      }
     }
   }
 
@@ -499,7 +602,15 @@ GmshConnectivityBuilder<3>::GmshConnectivityBuilder(const GmshReader::GmshData&
       point_list[j] = ref_point_list.second[j];
     }
     const GmshReader::PhysicalRefId& physical_ref_id = gmsh_data.m_physical_ref_map.at(ref_point_list.first);
-    descriptor.addRefItemList(RefNodeList(physical_ref_id.refId(), point_list));
+
+    bool is_boundary = true;
+    for (size_t i_node = 0; i_node < point_list.size(); ++i_node) {
+      if (not is_boundary_node[point_list[i_node]]) {
+        is_boundary = false;
+      }
+    }
+
+    descriptor.addRefItemList(RefNodeList(physical_ref_id.refId(), point_list, is_boundary));
   }
 
   descriptor.cell_owner_vector.resize(nb_cells);
@@ -1010,8 +1121,7 @@ GmshReader::__readPeriodic2_2()
 //   std::fill(descriptor.cell_owner_vector.begin(), descriptor.cell_owner_vector.end(), parallel::rank());
 
 //   descriptor.face_owner_vector.resize(descriptor.face_number_vector.size());
-//   std::fill(descriptor.face_owner_vector.begin(), descriptor.face_owner_vector.end(), parallel::rank());
-
+//   std::fill(descriptor.face_owner_vector.begin(), descriptor.face_owner_vector.end(), parallel::rank());//
 //   descriptor.edge_owner_vector.resize(descriptor.edge_number_vector.size());
 //   std::fill(descriptor.edge_owner_vector.begin(), descriptor.edge_owner_vector.end(), parallel::rank());
 
diff --git a/src/mesh/LogicalConnectivityBuilder.cpp b/src/mesh/LogicalConnectivityBuilder.cpp
index db14ff1df24cede3afcc7d77e96950b185274546..41092faaf90a9a3f9a24ac1c561304373ba748d0 100644
--- a/src/mesh/LogicalConnectivityBuilder.cpp
+++ b/src/mesh/LogicalConnectivityBuilder.cpp
@@ -24,13 +24,13 @@ LogicalConnectivityBuilder::_buildBoundaryNodeList(
   {   // xmin
     Array<NodeId> boundary_nodes(1);
     boundary_nodes[0] = 0;
-    descriptor.addRefItemList(RefNodeList{RefId{0, "XMIN"}, boundary_nodes});
+    descriptor.addRefItemList(RefNodeList{RefId{0, "XMIN"}, boundary_nodes, true});
   }
 
   {   // xmax
     Array<NodeId> boundary_nodes(1);
     boundary_nodes[0] = cell_size[0];
-    descriptor.addRefItemList(RefNodeList{RefId{1, "XMAX"}, boundary_nodes});
+    descriptor.addRefItemList(RefNodeList{RefId{1, "XMAX"}, boundary_nodes, true});
   }
 }
 
@@ -47,25 +47,25 @@ LogicalConnectivityBuilder::_buildBoundaryNodeList(
   {   // xminymin
     Array<NodeId> boundary_nodes(1);
     boundary_nodes[0] = node_number(0, 0);
-    descriptor.addRefItemList(RefNodeList{RefId{10, "XMINYMIN"}, boundary_nodes});
+    descriptor.addRefItemList(RefNodeList{RefId{10, "XMINYMIN"}, boundary_nodes, true});
   }
 
   {   // xmaxymin
     Array<NodeId> boundary_nodes(1);
     boundary_nodes[0] = node_number(cell_size[0], 0);
-    descriptor.addRefItemList(RefNodeList{RefId{11, "XMAXYMIN"}, boundary_nodes});
+    descriptor.addRefItemList(RefNodeList{RefId{11, "XMAXYMIN"}, boundary_nodes, true});
   }
 
   {   // xmaxymax
     Array<NodeId> boundary_nodes(1);
     boundary_nodes[0] = node_number(cell_size[0], cell_size[1]);
-    descriptor.addRefItemList(RefNodeList{RefId{12, "XMAXYMAX"}, boundary_nodes});
+    descriptor.addRefItemList(RefNodeList{RefId{12, "XMAXYMAX"}, boundary_nodes, true});
   }
 
   {   // xminymax
     Array<NodeId> boundary_nodes(1);
     boundary_nodes[0] = node_number(0, cell_size[1]);
-    descriptor.addRefItemList(RefNodeList{RefId{13, "XMINYMAX"}, boundary_nodes});
+    descriptor.addRefItemList(RefNodeList{RefId{13, "XMINYMAX"}, boundary_nodes, true});
   }
 }
 
@@ -83,49 +83,49 @@ LogicalConnectivityBuilder::_buildBoundaryNodeList(const TinyVector<3, uint64_t>
   {   // xminyminzmin
     Array<NodeId> boundary_nodes(1);
     boundary_nodes[0] = node_number(0, 0, 0);
-    descriptor.addRefItemList(RefNodeList{RefId{10, "XMINYMINZMIN"}, boundary_nodes});
+    descriptor.addRefItemList(RefNodeList{RefId{10, "XMINYMINZMIN"}, boundary_nodes, true});
   }
 
   {   // xmaxyminzmin
     Array<NodeId> boundary_nodes(1);
     boundary_nodes[0] = node_number(cell_size[0], 0, 0);
-    descriptor.addRefItemList(RefNodeList{RefId{11, "XMAXYMINZMIN"}, boundary_nodes});
+    descriptor.addRefItemList(RefNodeList{RefId{11, "XMAXYMINZMIN"}, boundary_nodes, true});
   }
 
   {   // xmaxymaxzmin
     Array<NodeId> boundary_nodes(1);
     boundary_nodes[0] = node_number(cell_size[0], cell_size[1], 0);
-    descriptor.addRefItemList(RefNodeList{RefId{12, "XMAXYMAXZMIN"}, boundary_nodes});
+    descriptor.addRefItemList(RefNodeList{RefId{12, "XMAXYMAXZMIN"}, boundary_nodes, true});
   }
 
   {   // xminymaxzmin
     Array<NodeId> boundary_nodes(1);
     boundary_nodes[0] = node_number(0, cell_size[1], 0);
-    descriptor.addRefItemList(RefNodeList{RefId{13, "XMINYMAXZMIN"}, boundary_nodes});
+    descriptor.addRefItemList(RefNodeList{RefId{13, "XMINYMAXZMIN"}, boundary_nodes, true});
   }
 
   {   // xminyminzmax
     Array<NodeId> boundary_nodes(1);
     boundary_nodes[0] = node_number(0, 0, cell_size[2]);
-    descriptor.addRefItemList(RefNodeList{RefId{14, "XMINYMINZMAX"}, boundary_nodes});
+    descriptor.addRefItemList(RefNodeList{RefId{14, "XMINYMINZMAX"}, boundary_nodes, true});
   }
 
   {   // xmaxyminzmax
     Array<NodeId> boundary_nodes(1);
     boundary_nodes[0] = node_number(cell_size[0], 0, cell_size[2]);
-    descriptor.addRefItemList(RefNodeList{RefId{15, "XMAXYMINZMAX"}, boundary_nodes});
+    descriptor.addRefItemList(RefNodeList{RefId{15, "XMAXYMINZMAX"}, boundary_nodes, true});
   }
 
   {   // xmaxymaxzmax
     Array<NodeId> boundary_nodes(1);
     boundary_nodes[0] = node_number(cell_size[0], cell_size[1], cell_size[2]);
-    descriptor.addRefItemList(RefNodeList{RefId{16, "XMAXYMAXZMAX"}, boundary_nodes});
+    descriptor.addRefItemList(RefNodeList{RefId{16, "XMAXYMAXZMAX"}, boundary_nodes, true});
   }
 
   {   // xminymaxzmax
     Array<NodeId> boundary_nodes(1);
     boundary_nodes[0] = node_number(0, cell_size[1], cell_size[2]);
-    descriptor.addRefItemList(RefNodeList{RefId{17, "XMINYMAXZMAX"}, boundary_nodes});
+    descriptor.addRefItemList(RefNodeList{RefId{17, "XMINYMAXZMAX"}, boundary_nodes, true});
   }
 }
 
@@ -181,7 +181,7 @@ LogicalConnectivityBuilder::_buildBoundaryEdgeList(const TinyVector<3, uint64_t>
         boundary_edges[l++] = i_edge->second;
       }
       Assert(l == cell_size[2]);
-      descriptor.addRefItemList(RefEdgeList{RefId{ref_id, ref_name}, boundary_edges});
+      descriptor.addRefItemList(RefEdgeList{RefId{ref_id, ref_name}, boundary_edges, true});
     };
 
     add_ref_item_list_along_z(0, 0, 20, "XMINYMIN");
@@ -205,7 +205,7 @@ LogicalConnectivityBuilder::_buildBoundaryEdgeList(const TinyVector<3, uint64_t>
         boundary_edges[l++] = i_edge->second;
       }
       Assert(l == cell_size[1]);
-      descriptor.addRefItemList(RefEdgeList{RefId{ref_id, ref_name}, boundary_edges});
+      descriptor.addRefItemList(RefEdgeList{RefId{ref_id, ref_name}, boundary_edges, true});
     };
 
     add_ref_item_list_along_y(0, 0, 24, "XMINZMIN");
@@ -229,7 +229,7 @@ LogicalConnectivityBuilder::_buildBoundaryEdgeList(const TinyVector<3, uint64_t>
         boundary_edges[l++] = i_edge->second;
       }
       Assert(l == cell_size[0]);
-      descriptor.addRefItemList(RefEdgeList{RefId{ref_id, ref_name}, boundary_edges});
+      descriptor.addRefItemList(RefEdgeList{RefId{ref_id, ref_name}, boundary_edges, true});
     };
 
     add_ref_item_list_along_x(0, 0, 28, "YMINZMIN");
@@ -267,7 +267,7 @@ LogicalConnectivityBuilder::_buildBoundaryFaceList(
 
       boundary_faces[j] = face_id;
     }
-    descriptor.addRefItemList(RefFaceList{RefId{0, "XMIN"}, boundary_faces});
+    descriptor.addRefItemList(RefFaceList{RefId{0, "XMIN"}, boundary_faces, true});
   }
 
   {   // xmax
@@ -281,7 +281,7 @@ LogicalConnectivityBuilder::_buildBoundaryFaceList(
 
       boundary_faces[j] = face_id;
     }
-    descriptor.addRefItemList(RefFaceList{RefId{1, "XMAX"}, boundary_faces});
+    descriptor.addRefItemList(RefFaceList{RefId{1, "XMAX"}, boundary_faces, true});
   }
 
   {   // ymin
@@ -295,7 +295,7 @@ LogicalConnectivityBuilder::_buildBoundaryFaceList(
 
       boundary_faces[i] = face_id;
     }
-    descriptor.addRefItemList(RefFaceList{RefId{2, "YMIN"}, boundary_faces});
+    descriptor.addRefItemList(RefFaceList{RefId{2, "YMIN"}, boundary_faces, true});
   }
 
   {   // ymax
@@ -309,7 +309,7 @@ LogicalConnectivityBuilder::_buildBoundaryFaceList(
 
       boundary_faces[i] = face_id;
     }
-    descriptor.addRefItemList(RefFaceList{RefId{3, "YMAX"}, boundary_faces});
+    descriptor.addRefItemList(RefFaceList{RefId{3, "YMAX"}, boundary_faces, true});
   }
 }
 
@@ -362,7 +362,7 @@ LogicalConnectivityBuilder::_buildBoundaryFaceList(const TinyVector<3, uint64_t>
         }
       }
       Assert(l == cell_size[1] * cell_size[2]);
-      descriptor.addRefItemList(RefFaceList{RefId{ref_id, ref_name}, boundary_faces});
+      descriptor.addRefItemList(RefFaceList{RefId{ref_id, ref_name}, boundary_faces, true});
     };
 
     add_ref_item_list_for_x(0, 0, "XMIN");
@@ -388,7 +388,7 @@ LogicalConnectivityBuilder::_buildBoundaryFaceList(const TinyVector<3, uint64_t>
         }
       }
       Assert(l == cell_size[0] * cell_size[2]);
-      descriptor.addRefItemList(RefFaceList{RefId{ref_id, ref_name}, boundary_faces});
+      descriptor.addRefItemList(RefFaceList{RefId{ref_id, ref_name}, boundary_faces, true});
     };
 
     add_ref_item_list_for_y(0, 2, "YMIN");
@@ -414,7 +414,7 @@ LogicalConnectivityBuilder::_buildBoundaryFaceList(const TinyVector<3, uint64_t>
         }
       }
       Assert(l == cell_size[0] * cell_size[1]);
-      descriptor.addRefItemList(RefFaceList{RefId{ref_id, ref_name}, boundary_faces});
+      descriptor.addRefItemList(RefFaceList{RefId{ref_id, ref_name}, boundary_faces, true});
     };
 
     add_ref_item_list_for_z(0, 4, "ZMIN");
diff --git a/src/mesh/MedianDualConnectivityBuilder.cpp b/src/mesh/MedianDualConnectivityBuilder.cpp
index 4ed951bc374c4f639b89e20e56264f838dfcf625..8ba8046c074355d702b6c3ec599d13232e080a0c 100644
--- a/src/mesh/MedianDualConnectivityBuilder.cpp
+++ b/src/mesh/MedianDualConnectivityBuilder.cpp
@@ -291,7 +291,8 @@ MedianDualConnectivityBuilder::_buildConnectivityFrom<2>(const IConnectivity& i_
       }();
 
       if (parallel::allReduceOr(dual_node_list.size() > 0)) {
-        dual_descriptor.addRefItemList(RefNodeList{primal_ref_node_list.refId(), convert_to_array(dual_node_list)});
+        dual_descriptor.addRefItemList(RefNodeList{primal_ref_node_list.refId(), convert_to_array(dual_node_list),
+                                                   primal_ref_node_list.isBoundary()});
       }
     }
   }
@@ -344,8 +345,9 @@ MedianDualConnectivityBuilder::_buildConnectivityFrom<2>(const IConnectivity& i_
     }();
 
     if (parallel::allReduceOr(boundary_dual_face_id_list.size() > 0)) {
-      dual_descriptor.addRefItemList(
-        RefFaceList{primal_ref_face_list.refId(), convert_to_array(boundary_dual_face_id_list)});
+      dual_descriptor.addRefItemList(RefFaceList{primal_ref_face_list.refId(),
+                                                 convert_to_array(boundary_dual_face_id_list),
+                                                 primal_ref_face_list.isBoundary()});
     }
   }
 
diff --git a/src/mesh/MeshCellZone.cpp b/src/mesh/MeshCellZone.cpp
index 240d8ea4f1be27e879d0455d312f12f67545e7fb..ac63998b6edc658f39b9b9a1a11cf72f3bcf1c0a 100644
--- a/src/mesh/MeshCellZone.cpp
+++ b/src/mesh/MeshCellZone.cpp
@@ -9,9 +9,6 @@ MeshCellZone<Dimension>::MeshCellZone(const Mesh<Connectivity<Dimension>>&, cons
   : m_cell_list(ref_cell_list.list()), m_zone_name(ref_cell_list.refId().tagName())
 {}
 
-template MeshCellZone<2>::MeshCellZone(const Mesh<Connectivity<2>>&, const RefCellList&);
-template MeshCellZone<3>::MeshCellZone(const Mesh<Connectivity<3>>&, const RefCellList&);
-
 template <size_t Dimension>
 MeshCellZone<Dimension>
 getMeshCellZone(const Mesh<Connectivity<Dimension>>& mesh, const IZoneDescriptor& zone_descriptor)
@@ -26,17 +23,7 @@ getMeshCellZone(const Mesh<Connectivity<Dimension>>& mesh, const IZoneDescriptor
   }
 
   std::ostringstream ost;
-  ost << "cannot find zone with name " << rang::fgB::red << zone_descriptor << rang::style::reset << '\n';
-  ost << "The mesh contains " << mesh.connectivity().template numberOfRefItemList<ItemType::cell>() << " zones: ";
-  for (size_t i_ref_cell_list = 0; i_ref_cell_list < mesh.connectivity().template numberOfRefItemList<ItemType::cell>();
-       ++i_ref_cell_list) {
-    const auto& ref_cell_list = mesh.connectivity().template refItemList<ItemType::cell>(i_ref_cell_list);
-    const RefId& ref          = ref_cell_list.refId();
-    if (i_ref_cell_list > 0) {
-      ost << ", ";
-    }
-    ost << rang::fgB::yellow << ref << rang::style::reset;
-  }
+  ost << "cannot find cell set with name \"" << rang::fgB::red << zone_descriptor << rang::style::reset << '\"';
 
   throw NormalError(ost.str());
 }
diff --git a/src/mesh/MeshEdgeBoundary.cpp b/src/mesh/MeshEdgeBoundary.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..cb522e5b01d3b2498459ec4bdd187693f8cf809f
--- /dev/null
+++ b/src/mesh/MeshEdgeBoundary.cpp
@@ -0,0 +1,110 @@
+#include <mesh/MeshEdgeBoundary.hpp>
+
+#include <Kokkos_Vector.hpp>
+#include <mesh/Connectivity.hpp>
+#include <mesh/Mesh.hpp>
+#include <utils/Messenger.hpp>
+
+template <size_t Dimension>
+MeshEdgeBoundary<Dimension>::MeshEdgeBoundary(const Mesh<Connectivity<Dimension>>&, const RefEdgeList& ref_edge_list)
+  : m_ref_edge_list(ref_edge_list)
+{}
+
+template MeshEdgeBoundary<1>::MeshEdgeBoundary(const Mesh<Connectivity<1>>&, const RefEdgeList&);
+template MeshEdgeBoundary<2>::MeshEdgeBoundary(const Mesh<Connectivity<2>>&, const RefEdgeList&);
+template MeshEdgeBoundary<3>::MeshEdgeBoundary(const Mesh<Connectivity<3>>&, const RefEdgeList&);
+
+template <size_t Dimension>
+MeshEdgeBoundary<Dimension>::MeshEdgeBoundary(const Mesh<Connectivity<Dimension>>& mesh,
+                                              const RefFaceList& ref_face_list)
+{
+  const Array<const FaceId>& face_list = ref_face_list.list();
+  static_assert(Dimension > 1, "conversion from to edge from face is valid in dimension > 1");
+
+  if constexpr (Dimension > 2) {
+    Kokkos::vector<unsigned int> edge_ids;
+    // not enough but should reduce significantly the number of resizing
+    edge_ids.reserve(Dimension * face_list.size());
+    const auto& face_to_edge_matrix = mesh.connectivity().faceToEdgeMatrix();
+
+    for (size_t l = 0; l < face_list.size(); ++l) {
+      const FaceId face_number = face_list[l];
+      const auto& face_edges   = face_to_edge_matrix[face_number];
+
+      for (size_t e = 0; e < face_edges.size(); ++e) {
+        edge_ids.push_back(face_edges[e]);
+      }
+    }
+    std::sort(edge_ids.begin(), edge_ids.end());
+    auto last = std::unique(edge_ids.begin(), edge_ids.end());
+    edge_ids.resize(std::distance(edge_ids.begin(), last));
+
+    Array<EdgeId> edge_list(edge_ids.size());
+    parallel_for(
+      edge_ids.size(), PUGS_LAMBDA(int r) { edge_list[r] = edge_ids[r]; });
+    m_ref_edge_list = RefEdgeList{ref_face_list.refId(), edge_list, ref_face_list.isBoundary()};
+  } else if constexpr (Dimension == 2) {
+    Array<EdgeId> edge_list(face_list.size());
+    parallel_for(
+      face_list.size(), PUGS_LAMBDA(int r) { edge_list[r] = static_cast<FaceId::base_type>(face_list[r]); });
+    m_ref_edge_list = RefEdgeList{ref_face_list.refId(), edge_list, ref_face_list.isBoundary()};
+  }
+
+  // This is quite dirty but it allows a non negligible performance
+  // improvement
+  const_cast<Connectivity<Dimension>&>(mesh.connectivity()).addRefItemList(m_ref_edge_list);
+}
+
+template MeshEdgeBoundary<2>::MeshEdgeBoundary(const Mesh<Connectivity<2>>&, const RefFaceList&);
+template MeshEdgeBoundary<3>::MeshEdgeBoundary(const Mesh<Connectivity<3>>&, const RefFaceList&);
+
+template <size_t Dimension>
+MeshEdgeBoundary<Dimension>
+getMeshEdgeBoundary(const Mesh<Connectivity<Dimension>>& mesh, const IBoundaryDescriptor& boundary_descriptor)
+{
+  for (size_t i_ref_edge_list = 0; i_ref_edge_list < mesh.connectivity().template numberOfRefItemList<ItemType::edge>();
+       ++i_ref_edge_list) {
+    const auto& ref_edge_list = mesh.connectivity().template refItemList<ItemType::edge>(i_ref_edge_list);
+    const RefId& ref          = ref_edge_list.refId();
+
+    if (ref == boundary_descriptor) {
+      auto edge_list = ref_edge_list.list();
+      if (not ref_edge_list.isBoundary()) {
+        std::ostringstream ost;
+        ost << "invalid boundary " << rang::fgB::yellow << boundary_descriptor << rang::style::reset
+            << ": inner edges cannot be used to define mesh boundaries";
+        throw NormalError(ost.str());
+      }
+
+      return MeshEdgeBoundary<Dimension>{mesh, ref_edge_list};
+    }
+  }
+  if constexpr (Dimension > 1) {
+    for (size_t i_ref_face_list = 0;
+         i_ref_face_list < mesh.connectivity().template numberOfRefItemList<ItemType::face>(); ++i_ref_face_list) {
+      const auto& ref_face_list = mesh.connectivity().template refItemList<ItemType::face>(i_ref_face_list);
+      const RefId& ref          = ref_face_list.refId();
+
+      if (ref == boundary_descriptor) {
+        auto face_list = ref_face_list.list();
+        if (not ref_face_list.isBoundary()) {
+          std::ostringstream ost;
+          ost << "invalid boundary " << rang::fgB::yellow << boundary_descriptor << rang::style::reset
+              << ": inner edges cannot be used to define mesh boundaries";
+          throw NormalError(ost.str());
+        }
+
+        return MeshEdgeBoundary<Dimension>{mesh, ref_face_list};
+      }
+    }
+  }
+
+  std::ostringstream ost;
+  ost << "cannot find edge list with name " << rang::fgB::red << boundary_descriptor << rang::style::reset;
+
+  throw NormalError(ost.str());
+}
+
+template MeshEdgeBoundary<1> getMeshEdgeBoundary(const Mesh<Connectivity<1>>&, const IBoundaryDescriptor&);
+template MeshEdgeBoundary<2> getMeshEdgeBoundary(const Mesh<Connectivity<2>>&, const IBoundaryDescriptor&);
+template MeshEdgeBoundary<3> getMeshEdgeBoundary(const Mesh<Connectivity<3>>&, const IBoundaryDescriptor&);
diff --git a/src/mesh/MeshEdgeBoundary.hpp b/src/mesh/MeshEdgeBoundary.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..2f2cd9a5222dd2e09e063965c2949c392bde12b0
--- /dev/null
+++ b/src/mesh/MeshEdgeBoundary.hpp
@@ -0,0 +1,60 @@
+#ifndef MESH_EDGE_BOUNDARY_HPP
+#define MESH_EDGE_BOUNDARY_HPP
+
+#include <algebra/TinyVector.hpp>
+#include <mesh/IBoundaryDescriptor.hpp>
+#include <mesh/RefItemList.hpp>
+#include <utils/Array.hpp>
+
+template <size_t Dimension>
+class Connectivity;
+
+template <typename ConnectivityType>
+class Mesh;
+
+template <size_t Dimension>
+class [[nodiscard]] MeshEdgeBoundary   // clazy:exclude=copyable-polymorphic
+{
+ protected:
+  RefEdgeList m_ref_edge_list;
+
+  std::array<TinyVector<Dimension>, Dimension*(Dimension - 1)> _getBounds(const Mesh<Connectivity<Dimension>>& mesh)
+    const;
+
+ public:
+  template <size_t MeshDimension>
+  friend MeshEdgeBoundary<MeshDimension> getMeshEdgeBoundary(const Mesh<Connectivity<MeshDimension>>& mesh,
+                                                             const IBoundaryDescriptor& boundary_descriptor);
+
+  MeshEdgeBoundary& operator=(const MeshEdgeBoundary&) = default;
+  MeshEdgeBoundary& operator=(MeshEdgeBoundary&&) = default;
+
+  PUGS_INLINE
+  const RefEdgeList& refEdgeList() const
+  {
+    return m_ref_edge_list;
+  }
+
+  PUGS_INLINE
+  const Array<const EdgeId>& edgeList() const
+  {
+    return m_ref_edge_list.list();
+  }
+
+ protected:
+  MeshEdgeBoundary(const Mesh<Connectivity<Dimension>>& mesh, const RefEdgeList& ref_edge_list);
+  MeshEdgeBoundary(const Mesh<Connectivity<Dimension>>& mesh, const RefFaceList& ref_face_list);
+
+ public:
+  MeshEdgeBoundary(const MeshEdgeBoundary&) = default;   // LCOV_EXCL_LINE
+  MeshEdgeBoundary(MeshEdgeBoundary &&)     = default;   // LCOV_EXCL_LINE
+
+  MeshEdgeBoundary()          = default;
+  virtual ~MeshEdgeBoundary() = default;
+};
+
+template <size_t Dimension>
+MeshEdgeBoundary<Dimension> getMeshEdgeBoundary(const Mesh<Connectivity<Dimension>>& mesh,
+                                                const IBoundaryDescriptor& boundary_descriptor);
+
+#endif   // MESH_EDGE_BOUNDARY_HPP
diff --git a/src/mesh/MeshFaceBoundary.cpp b/src/mesh/MeshFaceBoundary.cpp
index 3690c5c0c614a844060fa4c266e75fdcea42e377..ce18bd150ebb0810ae00b15bb7fc59ea919be7bd 100644
--- a/src/mesh/MeshFaceBoundary.cpp
+++ b/src/mesh/MeshFaceBoundary.cpp
@@ -6,9 +6,10 @@
 
 template <size_t Dimension>
 MeshFaceBoundary<Dimension>::MeshFaceBoundary(const Mesh<Connectivity<Dimension>>&, const RefFaceList& ref_face_list)
-  : m_face_list(ref_face_list.list()), m_boundary_name(ref_face_list.refId().tagName())
+  : m_ref_face_list(ref_face_list)
 {}
 
+template MeshFaceBoundary<1>::MeshFaceBoundary(const Mesh<Connectivity<1>>&, const RefFaceList&);
 template MeshFaceBoundary<2>::MeshFaceBoundary(const Mesh<Connectivity<2>>&, const RefFaceList&);
 template MeshFaceBoundary<3>::MeshFaceBoundary(const Mesh<Connectivity<3>>&, const RefFaceList&);
 
@@ -20,13 +21,22 @@ getMeshFaceBoundary(const Mesh<Connectivity<Dimension>>& mesh, const IBoundaryDe
        ++i_ref_face_list) {
     const auto& ref_face_list = mesh.connectivity().template refItemList<ItemType::face>(i_ref_face_list);
     const RefId& ref          = ref_face_list.refId();
+
     if (ref == boundary_descriptor) {
+      auto face_list = ref_face_list.list();
+      if (not ref_face_list.isBoundary()) {
+        std::ostringstream ost;
+        ost << "invalid boundary " << rang::fgB::yellow << boundary_descriptor << rang::style::reset
+            << ": inner faces cannot be used to define mesh boundaries";
+        throw NormalError(ost.str());
+      }
+
       return MeshFaceBoundary<Dimension>{mesh, ref_face_list};
     }
   }
 
   std::ostringstream ost;
-  ost << "cannot find surface with name " << rang::fgB::red << boundary_descriptor << rang::style::reset;
+  ost << "cannot find face list with name " << rang::fgB::red << boundary_descriptor << rang::style::reset;
 
   throw NormalError(ost.str());
 }
diff --git a/src/mesh/MeshFaceBoundary.hpp b/src/mesh/MeshFaceBoundary.hpp
index a45108aa5893c798230f41e14a8cacb071c60c0b..fd52128a3c960b43c5e432416c7e46a900d8eade 100644
--- a/src/mesh/MeshFaceBoundary.hpp
+++ b/src/mesh/MeshFaceBoundary.hpp
@@ -16,8 +16,7 @@ template <size_t Dimension>
 class [[nodiscard]] MeshFaceBoundary   // clazy:exclude=copyable-polymorphic
 {
  protected:
-  Array<const FaceId> m_face_list;
-  std::string m_boundary_name;
+  RefFaceList m_ref_face_list;
 
   std::array<TinyVector<Dimension>, Dimension*(Dimension - 1)> _getBounds(const Mesh<Connectivity<Dimension>>& mesh)
     const;
@@ -30,17 +29,24 @@ class [[nodiscard]] MeshFaceBoundary   // clazy:exclude=copyable-polymorphic
   MeshFaceBoundary& operator=(const MeshFaceBoundary&) = default;
   MeshFaceBoundary& operator=(MeshFaceBoundary&&) = default;
 
+  PUGS_INLINE
+  const RefFaceList& refFaceList() const
+  {
+    return m_ref_face_list;
+  }
+
+  PUGS_INLINE
   const Array<const FaceId>& faceList() const
   {
-    return m_face_list;
+    return m_ref_face_list.list();
   }
 
  protected:
   MeshFaceBoundary(const Mesh<Connectivity<Dimension>>& mesh, const RefFaceList& ref_face_list);
 
  public:
-  MeshFaceBoundary(const MeshFaceBoundary&) = default;
-  MeshFaceBoundary(MeshFaceBoundary &&)     = default;
+  MeshFaceBoundary(const MeshFaceBoundary&) = default;   // LCOV_EXCL_LINE
+  MeshFaceBoundary(MeshFaceBoundary &&)     = default;   // LCOV_EXCL_LINE
 
   MeshFaceBoundary()          = default;
   virtual ~MeshFaceBoundary() = default;
diff --git a/src/mesh/MeshFlatEdgeBoundary.cpp b/src/mesh/MeshFlatEdgeBoundary.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d4c1ec155a0e8c4d863facbf6280d71265944933
--- /dev/null
+++ b/src/mesh/MeshFlatEdgeBoundary.cpp
@@ -0,0 +1,20 @@
+#include <mesh/MeshFlatEdgeBoundary.hpp>
+
+#include <mesh/Connectivity.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshFlatNodeBoundary.hpp>
+
+template <size_t Dimension>
+MeshFlatEdgeBoundary<Dimension>
+getMeshFlatEdgeBoundary(const Mesh<Connectivity<Dimension>>& mesh, const IBoundaryDescriptor& boundary_descriptor)
+{
+  MeshEdgeBoundary<Dimension> mesh_edge_boundary          = getMeshEdgeBoundary(mesh, boundary_descriptor);
+  MeshFlatNodeBoundary<Dimension> mesh_flat_node_boundary = getMeshFlatNodeBoundary(mesh, boundary_descriptor);
+
+  return MeshFlatEdgeBoundary<Dimension>{mesh, mesh_edge_boundary.refEdgeList(),
+                                         mesh_flat_node_boundary.outgoingNormal()};
+}
+
+template MeshFlatEdgeBoundary<1> getMeshFlatEdgeBoundary(const Mesh<Connectivity<1>>&, const IBoundaryDescriptor&);
+template MeshFlatEdgeBoundary<2> getMeshFlatEdgeBoundary(const Mesh<Connectivity<2>>&, const IBoundaryDescriptor&);
+template MeshFlatEdgeBoundary<3> getMeshFlatEdgeBoundary(const Mesh<Connectivity<3>>&, const IBoundaryDescriptor&);
diff --git a/src/mesh/MeshFlatEdgeBoundary.hpp b/src/mesh/MeshFlatEdgeBoundary.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..a104d1413a7319f08b74bf1ef103a5157b803f18
--- /dev/null
+++ b/src/mesh/MeshFlatEdgeBoundary.hpp
@@ -0,0 +1,46 @@
+#ifndef MESH_FLAT_EDGE_BOUNDARY_HPP
+#define MESH_FLAT_EDGE_BOUNDARY_HPP
+
+#include <mesh/MeshEdgeBoundary.hpp>
+
+template <size_t Dimension>
+class MeshFlatEdgeBoundary final : public MeshEdgeBoundary<Dimension>   // clazy:exclude=copyable-polymorphic
+{
+ public:
+  using Rd = TinyVector<Dimension, double>;
+
+ private:
+  const Rd m_outgoing_normal;
+
+ public:
+  const Rd&
+  outgoingNormal() const
+  {
+    return m_outgoing_normal;
+  }
+
+  MeshFlatEdgeBoundary& operator=(const MeshFlatEdgeBoundary&) = default;
+  MeshFlatEdgeBoundary& operator=(MeshFlatEdgeBoundary&&) = default;
+
+  template <size_t MeshDimension>
+  friend MeshFlatEdgeBoundary<MeshDimension> getMeshFlatEdgeBoundary(const Mesh<Connectivity<MeshDimension>>& mesh,
+                                                                     const IBoundaryDescriptor& boundary_descriptor);
+
+ private:
+  template <typename MeshType>
+  MeshFlatEdgeBoundary(const MeshType& mesh, const RefEdgeList& ref_edge_list, const Rd& outgoing_normal)
+    : MeshEdgeBoundary<Dimension>(mesh, ref_edge_list), m_outgoing_normal(outgoing_normal)
+  {}
+
+ public:
+  MeshFlatEdgeBoundary()                            = default;
+  MeshFlatEdgeBoundary(const MeshFlatEdgeBoundary&) = default;
+  MeshFlatEdgeBoundary(MeshFlatEdgeBoundary&&)      = default;
+  ~MeshFlatEdgeBoundary()                           = default;
+};
+
+template <size_t Dimension>
+MeshFlatEdgeBoundary<Dimension> getMeshFlatEdgeBoundary(const Mesh<Connectivity<Dimension>>& mesh,
+                                                        const IBoundaryDescriptor& boundary_descriptor);
+
+#endif   // MESH_FLAT_EDGE_BOUNDARY_HPP
diff --git a/src/mesh/MeshFlatFaceBoundary.cpp b/src/mesh/MeshFlatFaceBoundary.cpp
index 0deefac0a8e9fc768634d53bfd5afa6d2323f5ec..87d353e25404c3b5996a63945a9e220e49e97e55 100644
--- a/src/mesh/MeshFlatFaceBoundary.cpp
+++ b/src/mesh/MeshFlatFaceBoundary.cpp
@@ -8,22 +8,13 @@ template <size_t Dimension>
 MeshFlatFaceBoundary<Dimension>
 getMeshFlatFaceBoundary(const Mesh<Connectivity<Dimension>>& mesh, const IBoundaryDescriptor& boundary_descriptor)
 {
-  for (size_t i_ref_face_list = 0; i_ref_face_list < mesh.connectivity().template numberOfRefItemList<ItemType::face>();
-       ++i_ref_face_list) {
-    const auto& ref_face_list = mesh.connectivity().template refItemList<ItemType::face>(i_ref_face_list);
-    const RefId& ref          = ref_face_list.refId();
-    if (ref == boundary_descriptor) {
-      MeshFlatNodeBoundary<Dimension> mesh_flat_node_boundary = getMeshFlatNodeBoundary(mesh, boundary_descriptor);
+  MeshFaceBoundary<Dimension> mesh_face_boundary          = getMeshFaceBoundary(mesh, boundary_descriptor);
+  MeshFlatNodeBoundary<Dimension> mesh_flat_node_boundary = getMeshFlatNodeBoundary(mesh, boundary_descriptor);
 
-      return MeshFlatFaceBoundary<Dimension>{mesh, ref_face_list, mesh_flat_node_boundary.outgoingNormal()};
-    }
-  }
-
-  std::ostringstream ost;
-  ost << "cannot find surface with name " << rang::fgB::red << boundary_descriptor << rang::style::reset;
-
-  throw NormalError(ost.str());
+  return MeshFlatFaceBoundary<Dimension>{mesh, mesh_face_boundary.refFaceList(),
+                                         mesh_flat_node_boundary.outgoingNormal()};
 }
 
+template MeshFlatFaceBoundary<1> getMeshFlatFaceBoundary(const Mesh<Connectivity<1>>&, const IBoundaryDescriptor&);
 template MeshFlatFaceBoundary<2> getMeshFlatFaceBoundary(const Mesh<Connectivity<2>>&, const IBoundaryDescriptor&);
 template MeshFlatFaceBoundary<3> getMeshFlatFaceBoundary(const Mesh<Connectivity<3>>&, const IBoundaryDescriptor&);
diff --git a/src/mesh/MeshFlatNodeBoundary.cpp b/src/mesh/MeshFlatNodeBoundary.cpp
index 059d6b004e1f3eaff9c9940dec3f2af7b8fbf57c..b7de42715eff88510119b54507ed32323365c817 100644
--- a/src/mesh/MeshFlatNodeBoundary.cpp
+++ b/src/mesh/MeshFlatNodeBoundary.cpp
@@ -15,17 +15,18 @@ MeshFlatNodeBoundary<Dimension>::_checkBoundaryIsFlat(const TinyVector<Dimension
 
   bool is_bad = false;
 
-  parallel_for(this->m_node_list.size(), [=, &is_bad](int r) {
-    const Rd& x = xr[this->m_node_list[r]];
-    if (dot(x - origin, normal) > 1E-13 * length) {
+  auto node_list = this->m_ref_node_list.list();
+  parallel_for(node_list.size(), [=, &is_bad](int r) {
+    const Rd& x = xr[node_list[r]];
+    if (std::abs(dot(x - origin, normal)) > 1E-13 * length) {
       is_bad = true;
     }
   });
 
   if (parallel::allReduceOr(is_bad)) {
     std::ostringstream ost;
-    ost << "invalid boundary " << rang::fgB::yellow << this->m_boundary_name << rang::style::reset
-        << ": boundary is not flat!";
+    ost << "invalid boundary \"" << rang::fgB::yellow << this->m_ref_node_list.refId() << rang::style::reset
+        << "\": boundary is not flat!";
     throw NormalError(ost.str());
   }
 }
@@ -39,15 +40,16 @@ MeshFlatNodeBoundary<1>::_getNormal(const Mesh<Connectivity<1>>& mesh)
   const size_t number_of_bc_nodes = [&]() {
     size_t number_of_bc_nodes = 0;
     auto node_is_owned        = mesh.connectivity().nodeIsOwned();
-    for (size_t i_node = 0; i_node < m_node_list.size(); ++i_node) {
-      number_of_bc_nodes += (node_is_owned[m_node_list[i_node]]);
+    auto node_list            = m_ref_node_list.list();
+    for (size_t i_node = 0; i_node < node_list.size(); ++i_node) {
+      number_of_bc_nodes += (node_is_owned[node_list[i_node]]);
     }
     return parallel::allReduceMax(number_of_bc_nodes);
   }();
 
   if (number_of_bc_nodes != 1) {
     std::ostringstream ost;
-    ost << "invalid boundary " << rang::fgB::yellow << m_boundary_name << rang::style::reset
+    ost << "invalid boundary " << rang::fgB::yellow << m_ref_node_list.refId() << rang::style::reset
         << ": node boundaries in 1D require to have exactly 1 node";
     throw NormalError(ost.str());
   }
@@ -68,8 +70,8 @@ MeshFlatNodeBoundary<2>::_getNormal(const Mesh<Connectivity<2>>& mesh)
 
   if (xmin == xmax) {
     std::ostringstream ost;
-    ost << "invalid boundary " << rang::fgB::yellow << this->m_boundary_name << rang::style::reset
-        << ": unable to compute normal";
+    ost << "invalid boundary \"" << rang::fgB::yellow << m_ref_node_list.refId() << rang::style::reset
+        << "\": unable to compute normal";
     throw NormalError(ost.str());
   }
 
@@ -126,8 +128,8 @@ MeshFlatNodeBoundary<3>::_getNormal(const Mesh<Connectivity<3>>& mesh)
 
   if (normal_l2 == 0) {
     std::ostringstream ost;
-    ost << "invalid boundary " << rang::fgB::yellow << this->m_boundary_name << rang::style::reset
-        << ": unable to compute normal";
+    ost << "invalid boundary \"" << rang::fgB::yellow << m_ref_node_list.refId() << rang::style::reset
+        << "\": unable to compute normal";
     throw NormalError(ost.str());
   }
 
@@ -149,14 +151,14 @@ MeshFlatNodeBoundary<1>::_getOutgoingNormal(const Mesh<Connectivity<1>>& mesh)
   const R normal = this->_getNormal(mesh);
 
   double max_height = 0;
-
-  if (m_node_list.size() > 0) {
+  auto node_list    = m_ref_node_list.list();
+  if (node_list.size() > 0) {
     const NodeValue<const R>& xr    = mesh.xr();
     const auto& cell_to_node_matrix = mesh.connectivity().cellToNodeMatrix();
 
     const auto& node_to_cell_matrix = mesh.connectivity().nodeToCellMatrix();
 
-    const NodeId r0      = m_node_list[0];
+    const NodeId r0      = node_list[0];
     const CellId j0      = node_to_cell_matrix[r0][0];
     const auto& j0_nodes = cell_to_node_matrix[j0];
 
@@ -193,13 +195,14 @@ MeshFlatNodeBoundary<2>::_getOutgoingNormal(const Mesh<Connectivity<2>>& mesh)
 
   double max_height = 0;
 
-  if (m_node_list.size() > 0) {
+  auto node_list = m_ref_node_list.list();
+  if (node_list.size() > 0) {
     const NodeValue<const R2>& xr   = mesh.xr();
     const auto& cell_to_node_matrix = mesh.connectivity().cellToNodeMatrix();
 
     const auto& node_to_cell_matrix = mesh.connectivity().nodeToCellMatrix();
 
-    const NodeId r0      = m_node_list[0];
+    const NodeId r0      = node_list[0];
     const CellId j0      = node_to_cell_matrix[r0][0];
     const auto& j0_nodes = cell_to_node_matrix[j0];
     for (size_t r = 0; r < j0_nodes.size(); ++r) {
@@ -234,14 +237,14 @@ MeshFlatNodeBoundary<3>::_getOutgoingNormal(const Mesh<Connectivity<3>>& mesh)
   const R3 normal = this->_getNormal(mesh);
 
   double max_height = 0;
-
-  if (m_node_list.size() > 0) {
+  auto node_list    = m_ref_node_list.list();
+  if (node_list.size() > 0) {
     const NodeValue<const R3>& xr   = mesh.xr();
     const auto& cell_to_node_matrix = mesh.connectivity().cellToNodeMatrix();
 
     const auto& node_to_cell_matrix = mesh.connectivity().nodeToCellMatrix();
 
-    const NodeId r0      = m_node_list[0];
+    const NodeId r0      = node_list[0];
     const CellId j0      = node_to_cell_matrix[r0][0];
     const auto& j0_nodes = cell_to_node_matrix[j0];
 
diff --git a/src/mesh/MeshLineEdgeBoundary.cpp b/src/mesh/MeshLineEdgeBoundary.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..16e2e3e9764d1465cd767c130fbf27b0e7589afd
--- /dev/null
+++ b/src/mesh/MeshLineEdgeBoundary.cpp
@@ -0,0 +1,19 @@
+#include <mesh/MeshLineEdgeBoundary.hpp>
+
+#include <mesh/Connectivity.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshLineNodeBoundary.hpp>
+#include <utils/Messenger.hpp>
+
+template <size_t Dimension>
+MeshLineEdgeBoundary<Dimension>
+getMeshLineEdgeBoundary(const Mesh<Connectivity<Dimension>>& mesh, const IBoundaryDescriptor& boundary_descriptor)
+{
+  MeshEdgeBoundary<Dimension> mesh_edge_boundary          = getMeshEdgeBoundary(mesh, boundary_descriptor);
+  MeshLineNodeBoundary<Dimension> mesh_line_node_boundary = getMeshLineNodeBoundary(mesh, boundary_descriptor);
+
+  return MeshLineEdgeBoundary<Dimension>{mesh, mesh_edge_boundary.refEdgeList(), mesh_line_node_boundary.direction()};
+}
+
+template MeshLineEdgeBoundary<2> getMeshLineEdgeBoundary(const Mesh<Connectivity<2>>&, const IBoundaryDescriptor&);
+template MeshLineEdgeBoundary<3> getMeshLineEdgeBoundary(const Mesh<Connectivity<3>>&, const IBoundaryDescriptor&);
diff --git a/src/mesh/MeshLineEdgeBoundary.hpp b/src/mesh/MeshLineEdgeBoundary.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..160444648524f266941a8d2903124bf91b77ee8b
--- /dev/null
+++ b/src/mesh/MeshLineEdgeBoundary.hpp
@@ -0,0 +1,49 @@
+#ifndef MESH_LINE_EDGE_BOUNDARY_HPP
+#define MESH_LINE_EDGE_BOUNDARY_HPP
+
+#include <algebra/TinyMatrix.hpp>
+#include <mesh/MeshEdgeBoundary.hpp>
+
+template <size_t Dimension>
+class [[nodiscard]] MeshLineEdgeBoundary final
+  : public MeshEdgeBoundary<Dimension>   // clazy:exclude=copyable-polymorphic
+{
+ public:
+  static_assert(Dimension > 1, "MeshLineEdgeBoundary makes only sense in dimension 1");
+
+  using Rd = TinyVector<Dimension, double>;
+
+ private:
+  const Rd m_direction;
+
+ public:
+  template <size_t MeshDimension>
+  friend MeshLineEdgeBoundary<MeshDimension> getMeshLineEdgeBoundary(const Mesh<Connectivity<MeshDimension>>& mesh,
+                                                                     const IBoundaryDescriptor& boundary_descriptor);
+
+  PUGS_INLINE
+  const Rd& direction() const
+  {
+    return m_direction;
+  }
+
+  MeshLineEdgeBoundary& operator=(const MeshLineEdgeBoundary&) = default;
+  MeshLineEdgeBoundary& operator=(MeshLineEdgeBoundary&&) = default;
+
+ private:
+  MeshLineEdgeBoundary(const Mesh<Connectivity<Dimension>>& mesh, const RefEdgeList& ref_edge_list, const Rd& direction)
+    : MeshEdgeBoundary<Dimension>(mesh, ref_edge_list), m_direction(direction)
+  {}
+
+ public:
+  MeshLineEdgeBoundary()                            = default;
+  MeshLineEdgeBoundary(const MeshLineEdgeBoundary&) = default;
+  MeshLineEdgeBoundary(MeshLineEdgeBoundary &&)     = default;
+  ~MeshLineEdgeBoundary()                           = default;
+};
+
+template <size_t Dimension>
+MeshLineEdgeBoundary<Dimension> getMeshLineEdgeBoundary(const Mesh<Connectivity<Dimension>>& mesh,
+                                                        const IBoundaryDescriptor& boundary_descriptor);
+
+#endif   // MESH_LINE_EDGE_BOUNDARY_HPP
diff --git a/src/mesh/MeshLineFaceBoundary.cpp b/src/mesh/MeshLineFaceBoundary.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..cd67176349d0edb475001c58acc99d9cac9792ec
--- /dev/null
+++ b/src/mesh/MeshLineFaceBoundary.cpp
@@ -0,0 +1,18 @@
+#include <mesh/MeshLineFaceBoundary.hpp>
+
+#include <mesh/Connectivity.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshLineNodeBoundary.hpp>
+#include <utils/Messenger.hpp>
+
+template <size_t Dimension>
+MeshLineFaceBoundary<Dimension>
+getMeshLineFaceBoundary(const Mesh<Connectivity<Dimension>>& mesh, const IBoundaryDescriptor& boundary_descriptor)
+{
+  MeshFaceBoundary<Dimension> mesh_face_boundary          = getMeshFaceBoundary(mesh, boundary_descriptor);
+  MeshLineNodeBoundary<Dimension> mesh_line_node_boundary = getMeshLineNodeBoundary(mesh, boundary_descriptor);
+
+  return MeshLineFaceBoundary<Dimension>{mesh, mesh_face_boundary.refFaceList(), mesh_line_node_boundary.direction()};
+}
+
+template MeshLineFaceBoundary<2> getMeshLineFaceBoundary(const Mesh<Connectivity<2>>&, const IBoundaryDescriptor&);
diff --git a/src/mesh/MeshLineFaceBoundary.hpp b/src/mesh/MeshLineFaceBoundary.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..5631361f5f995883d80c5317a178eab122ddcda9
--- /dev/null
+++ b/src/mesh/MeshLineFaceBoundary.hpp
@@ -0,0 +1,49 @@
+#ifndef MESH_LINE_FACE_BOUNDARY_HPP
+#define MESH_LINE_FACE_BOUNDARY_HPP
+
+#include <algebra/TinyMatrix.hpp>
+#include <mesh/MeshFaceBoundary.hpp>
+
+template <size_t Dimension>
+class [[nodiscard]] MeshLineFaceBoundary final
+  : public MeshFaceBoundary<Dimension>   // clazy:exclude=copyable-polymorphic
+{
+ public:
+  static_assert(Dimension == 2, "MeshLineFaceBoundary makes only sense in dimension 2");
+
+  using Rd = TinyVector<Dimension, double>;
+
+ private:
+  const Rd m_direction;
+
+ public:
+  template <size_t MeshDimension>
+  friend MeshLineFaceBoundary<MeshDimension> getMeshLineFaceBoundary(const Mesh<Connectivity<MeshDimension>>& mesh,
+                                                                     const IBoundaryDescriptor& boundary_descriptor);
+
+  PUGS_INLINE
+  const Rd& direction() const
+  {
+    return m_direction;
+  }
+
+  MeshLineFaceBoundary& operator=(const MeshLineFaceBoundary&) = default;
+  MeshLineFaceBoundary& operator=(MeshLineFaceBoundary&&) = default;
+
+ private:
+  MeshLineFaceBoundary(const Mesh<Connectivity<Dimension>>& mesh, const RefFaceList& ref_face_list, const Rd& direction)
+    : MeshFaceBoundary<Dimension>(mesh, ref_face_list), m_direction(direction)
+  {}
+
+ public:
+  MeshLineFaceBoundary()                            = default;
+  MeshLineFaceBoundary(const MeshLineFaceBoundary&) = default;
+  MeshLineFaceBoundary(MeshLineFaceBoundary &&)     = default;
+  ~MeshLineFaceBoundary()                           = default;
+};
+
+template <size_t Dimension>
+MeshLineFaceBoundary<Dimension> getMeshLineFaceBoundary(const Mesh<Connectivity<Dimension>>& mesh,
+                                                        const IBoundaryDescriptor& boundary_descriptor);
+
+#endif   // MESH_LINE_FACE_BOUNDARY_HPP
diff --git a/src/mesh/MeshLineNodeBoundary.cpp b/src/mesh/MeshLineNodeBoundary.cpp
index cae862f33dd9a60d5b0e895aba4907cf5a5b102a..1b126f6207719d648415edbc41be6bb83c36abe7 100644
--- a/src/mesh/MeshLineNodeBoundary.cpp
+++ b/src/mesh/MeshLineNodeBoundary.cpp
@@ -15,33 +15,26 @@ MeshLineNodeBoundary<Dimension>::_checkBoundaryIsLine(const TinyVector<Dimension
 
   const NodeValue<const Rd>& xr = mesh.xr();
 
-  Rdxd P = Rdxd{identity} - tensorProduct(direction, direction);
-
-  bool is_bad = false;
-  parallel_for(this->m_node_list.size(), [=, &is_bad](int node_id) {
-    const Rd& x    = xr[this->m_node_list[node_id]];
-    const Rd delta = x - origin;
-    if (dot(P * delta, direction) > 1E-13 * length) {
+  const Rdxd P = Rdxd{identity} - tensorProduct(direction, direction);
+
+  bool is_bad    = false;
+  auto node_list = this->m_ref_node_list.list();
+  parallel_for(node_list.size(), [=, &is_bad](int i_node) {
+    const Rd& x    = xr[node_list[i_node]];
+    const Rd delta = P * (x - origin);
+    if (dot(delta, delta) > 1E-13 * length) {
       is_bad = true;
     }
   });
 
   if (parallel::allReduceOr(is_bad)) {
     std::ostringstream ost;
-    ost << "invalid boundary " << rang::fgB::yellow << this->m_boundary_name << rang::style::reset
-        << ": boundary is not a line!";
+    ost << "invalid boundary \"" << rang::fgB::yellow << this->m_ref_node_list.refId() << rang::style::reset
+        << "\": boundary is not a line!";
     throw NormalError(ost.str());
   }
 }
 
-template <>
-template <>
-TinyVector<1>
-MeshLineNodeBoundary<1>::_getDirection(const Mesh<Connectivity<1>>&)
-{
-  throw UnexpectedError("MeshLineNodeBoundary makes no sense in dimension 1");
-}
-
 template <>
 template <>
 TinyVector<2>
@@ -56,8 +49,8 @@ MeshLineNodeBoundary<2>::_getDirection(const Mesh<Connectivity<2>>& mesh)
 
   if (xmin == xmax) {
     std::ostringstream ost;
-    ost << "invalid boundary " << rang::fgB::yellow << this->m_boundary_name << rang::style::reset
-        << ": unable to compute direction";
+    ost << "invalid boundary \"" << rang::fgB::yellow << m_ref_node_list.refId() << rang::style::reset
+        << "\": unable to compute direction";
     throw NormalError(ost.str());
   }
 
@@ -100,6 +93,12 @@ MeshLineNodeBoundary<3>::_getDirection(const Mesh<Connectivity<3>>& mesh)
   }
 
   const double length = l2Norm(direction);
+  if (length == 0) {
+    std::ostringstream ost;
+    ost << "invalid boundary \"" << rang::fgB::yellow << this->m_ref_node_list.refId() << rang::style::reset
+        << "\": unable to compute direction";
+    throw NormalError(ost.str());
+  }
   direction *= 1. / length;
 
   this->_checkBoundaryIsLine(direction, xmin, length, mesh);
@@ -142,6 +141,5 @@ getMeshLineNodeBoundary(const Mesh<Connectivity<Dimension>>& mesh, const IBounda
   throw NormalError(ost.str());
 }
 
-template MeshLineNodeBoundary<1> getMeshLineNodeBoundary(const Mesh<Connectivity<1>>&, const IBoundaryDescriptor&);
 template MeshLineNodeBoundary<2> getMeshLineNodeBoundary(const Mesh<Connectivity<2>>&, const IBoundaryDescriptor&);
 template MeshLineNodeBoundary<3> getMeshLineNodeBoundary(const Mesh<Connectivity<3>>&, const IBoundaryDescriptor&);
diff --git a/src/mesh/MeshLineNodeBoundary.hpp b/src/mesh/MeshLineNodeBoundary.hpp
index 8694dd257247eb5aeaf2224d113a34c9c4c8fa32..730b9f536bbf5563d656b59c0d246ff71193ea57 100644
--- a/src/mesh/MeshLineNodeBoundary.hpp
+++ b/src/mesh/MeshLineNodeBoundary.hpp
@@ -9,6 +9,8 @@ class [[nodiscard]] MeshLineNodeBoundary final
   : public MeshNodeBoundary<Dimension>   // clazy:exclude=copyable-polymorphic
 {
  public:
+  static_assert(Dimension > 1, "MeshLineNodeBoundary makes only sense in dimension greater than 1");
+
   using Rd = TinyVector<Dimension, double>;
 
  private:
diff --git a/src/mesh/MeshNodeBoundary.cpp b/src/mesh/MeshNodeBoundary.cpp
index 56ee047545d51e20f11e7b0421dae839615bf2cc..3e01616c66670706449018107aa3d417ab1d5007 100644
--- a/src/mesh/MeshNodeBoundary.cpp
+++ b/src/mesh/MeshNodeBoundary.cpp
@@ -32,8 +32,9 @@ MeshNodeBoundary<2>::_getBounds(const Mesh<Connectivity<2>>& mesh) const
     }
   };
 
-  for (size_t r = 0; r < m_node_list.size(); ++r) {
-    const R2& x = xr[m_node_list[r]];
+  auto node_list = m_ref_node_list.list();
+  for (size_t r = 0; r < node_list.size(); ++r) {
+    const R2& x = xr[node_list[r]];
     update_xmin(x, xmin);
     update_xmax(x, xmax);
   }
@@ -85,23 +86,23 @@ MeshNodeBoundary<3>::_getBounds(const Mesh<Connectivity<3>>& mesh) const
   auto update_ymax = [](const R3& x, R3& ymax) {
     // YMAX: X.ymax X.zmin X.xmax
     if ((x[1] > ymax[1]) or ((x[1] == ymax[1]) and (x[2] < ymax[2])) or
-        ((x[1] == ymax[1]) and (x[2] == ymax[1]) and (x[0] > ymax[0]))) {
+        ((x[1] == ymax[1]) and (x[2] == ymax[2]) and (x[0] > ymax[0]))) {
       ymax = x;
     }
   };
 
   auto update_zmin = [](const R3& x, R3& zmin) {
     // ZMIN: X.zmin X.xmin X.ymin
-    if ((x[2] < zmin[2]) or ((x[2] == zmin[2]) and (x[2] < zmin[2])) or
-        ((x[1] == zmin[1]) and (x[2] == zmin[1]) and (x[0] < zmin[0]))) {
+    if ((x[2] < zmin[2]) or ((x[2] == zmin[2]) and (x[0] < zmin[0])) or
+        ((x[2] == zmin[2]) and (x[0] == zmin[0]) and (x[1] < zmin[1]))) {
       zmin = x;
     }
   };
 
   auto update_zmax = [](const R3& x, R3& zmax) {
     // ZMAX: X.zmax X.xmax X.ymax
-    if ((x[2] > zmax[2]) or ((x[2] == zmax[2]) and (x[2] > zmax[2])) or
-        ((x[1] == zmax[1]) and (x[2] == zmax[1]) and (x[0] > zmax[0]))) {
+    if ((x[2] > zmax[2]) or ((x[2] == zmax[2]) and (x[0] > zmax[0])) or
+        ((x[2] == zmax[2]) and (x[0] == zmax[0]) and (x[1] > zmax[1]))) {
       zmax = x;
     }
   };
@@ -115,31 +116,35 @@ MeshNodeBoundary<3>::_getBounds(const Mesh<Connectivity<3>>& mesh) const
   R3& zmax = bounds[5];
 
   xmin = R3{std::numeric_limits<double>::max(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max()};
-  ymin = xmin;
-  zmin = xmin;
+  ymin = R3{std::numeric_limits<double>::max(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max()};
+  zmin = R3{std::numeric_limits<double>::max(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max()};
 
-  xmax = -xmin;
-  ymax = xmax;
-  zmax = xmax;
+  xmax =
+    -R3{std::numeric_limits<double>::max(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max()};
+  ymax =
+    -R3{std::numeric_limits<double>::max(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max()};
+  zmax =
+    -R3{std::numeric_limits<double>::max(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max()};
 
   const NodeValue<const R3>& xr = mesh.xr();
 
-  for (size_t r = 0; r < m_node_list.size(); ++r) {
-    const R3& x = xr[m_node_list[r]];
+  auto node_list = m_ref_node_list.list();
+  for (size_t r = 0; r < node_list.size(); ++r) {
+    const R3& x = xr[node_list[r]];
     update_xmin(x, xmin);
-    update_xmax(x, xmax);
     update_ymin(x, ymin);
-    update_ymax(x, ymax);
     update_zmin(x, zmin);
+    update_xmax(x, xmax);
+    update_ymax(x, ymax);
     update_zmax(x, zmax);
   }
 
-  if (parallel::size() > 0) {
+  if (parallel::size() > 1) {
     Array<const R3> xmin_array = parallel::allGather(xmin);
-    Array<const R3> xmax_array = parallel::allGather(xmax);
     Array<const R3> ymin_array = parallel::allGather(ymin);
-    Array<const R3> ymax_array = parallel::allGather(ymax);
     Array<const R3> zmin_array = parallel::allGather(zmin);
+    Array<const R3> xmax_array = parallel::allGather(xmax);
+    Array<const R3> ymax_array = parallel::allGather(ymax);
     Array<const R3> zmax_array = parallel::allGather(zmax);
 
     for (size_t i = 0; i < xmin_array.size(); ++i) {
@@ -168,24 +173,12 @@ MeshNodeBoundary<3>::_getBounds(const Mesh<Connectivity<3>>& mesh) const
 template <size_t Dimension>
 MeshNodeBoundary<Dimension>::MeshNodeBoundary(const Mesh<Connectivity<Dimension>>& mesh,
                                               const RefFaceList& ref_face_list)
-  : m_boundary_name(ref_face_list.refId().tagName())
 {
-  const auto& face_to_cell_matrix = mesh.connectivity().faceToCellMatrix();
-
   const Array<const FaceId>& face_list = ref_face_list.list();
-
-  bool is_bad = false;
-  parallel_for(face_list.size(), [=, &is_bad](int l) {
-    const auto& face_cells = face_to_cell_matrix[face_list[l]];
-    if (face_cells.size() > 1) {
-      is_bad = true;
-    }
-  });
-
-  if (parallel::allReduceOr(is_bad)) {
+  if (not ref_face_list.isBoundary()) {
     std::ostringstream ost;
-    ost << "invalid boundary " << rang::fgB::yellow << this->m_boundary_name << rang::style::reset
-        << ": inner faces cannot be used to define mesh boundaries";
+    ost << "invalid boundary \"" << rang::fgB::yellow << ref_face_list.refId() << rang::style::reset
+        << "\": inner faces cannot be used to define mesh boundaries";
     throw NormalError(ost.str());
   }
 
@@ -210,62 +203,28 @@ MeshNodeBoundary<Dimension>::MeshNodeBoundary(const Mesh<Connectivity<Dimension>
     Array<NodeId> node_list(node_ids.size());
     parallel_for(
       node_ids.size(), PUGS_LAMBDA(int r) { node_list[r] = node_ids[r]; });
-    m_node_list = node_list;
+    m_ref_node_list = RefNodeList{ref_face_list.refId(), node_list, ref_face_list.isBoundary()};
   } else {
     Array<NodeId> node_list(face_list.size());
     parallel_for(
       face_list.size(), PUGS_LAMBDA(int r) { node_list[r] = static_cast<FaceId::base_type>(face_list[r]); });
-    m_node_list = node_list;
+    m_ref_node_list = RefNodeList{ref_face_list.refId(), node_list, ref_face_list.isBoundary()};
   }
+
+  // This is quite dirty but it allows a non negligible performance
+  // improvement
+  const_cast<Connectivity<Dimension>&>(mesh.connectivity()).addRefItemList(m_ref_node_list);
 }
 
 template <size_t Dimension>
 MeshNodeBoundary<Dimension>::MeshNodeBoundary(const Mesh<Connectivity<Dimension>>& mesh,
                                               const RefEdgeList& ref_edge_list)
-  : m_boundary_name(ref_edge_list.refId().tagName())
 {
   const Array<const EdgeId>& edge_list = ref_edge_list.list();
-  const auto& edge_is_owned            = mesh.connectivity().edgeIsOwned();
-
-  bool is_bad = false;
-
-  if constexpr ((Dimension == 1) or (Dimension == 2)) {
-    const auto& edge_to_cell_matrix = mesh.connectivity().edgeToCellMatrix();
-    parallel_for(edge_list.size(), [=, &is_bad](int l) {
-      const EdgeId edge_id = edge_list[l];
-      if (edge_is_owned[edge_id]) {
-        if (edge_to_cell_matrix[edge_id].size() != 1) {
-          is_bad = true;
-        }
-      }
-    });
-  } else {
-    static_assert(Dimension == 3);
-    const auto& edge_to_face_matrix = mesh.connectivity().edgeToFaceMatrix();
-    const auto& face_to_cell_matrix = mesh.connectivity().faceToCellMatrix();
-
-    parallel_for(edge_list.size(), [=, &is_bad](int l) {
-      const EdgeId edge_id = edge_list[l];
-      if (edge_is_owned[edge_id]) {
-        const auto& edge_faces             = edge_to_face_matrix[edge_id];
-        bool is_connected_to_boundary_face = false;
-        for (size_t i_edge_face = 0; i_edge_face < edge_faces.size(); ++i_edge_face) {
-          const FaceId edge_face_id = edge_faces[i_edge_face];
-          if (face_to_cell_matrix[edge_face_id].size() == 1) {
-            is_connected_to_boundary_face = true;
-          }
-        }
-        if (not is_connected_to_boundary_face) {
-          is_bad = true;
-        }
-      }
-    });
-  }
-
-  if (parallel::allReduceOr(is_bad)) {
+  if (not ref_edge_list.isBoundary()) {
     std::ostringstream ost;
-    ost << "invalid boundary " << rang::fgB::yellow << this->m_boundary_name << rang::style::reset
-        << ": inner edges cannot be used to define mesh boundaries";
+    ost << "invalid boundary \"" << rang::fgB::yellow << ref_edge_list.refId() << rang::style::reset
+        << "\": inner edges cannot be used to define mesh boundaries";
     throw NormalError(ost.str());
   }
 
@@ -289,19 +248,30 @@ MeshNodeBoundary<Dimension>::MeshNodeBoundary(const Mesh<Connectivity<Dimension>
     Array<NodeId> node_list(node_ids.size());
     parallel_for(
       node_ids.size(), PUGS_LAMBDA(int r) { node_list[r] = node_ids[r]; });
-    m_node_list = node_list;
+    m_ref_node_list = RefNodeList{ref_edge_list.refId(), node_list, ref_edge_list.isBoundary()};
   } else {
     Array<NodeId> node_list(edge_list.size());
     parallel_for(
       edge_list.size(), PUGS_LAMBDA(int r) { node_list[r] = static_cast<EdgeId::base_type>(edge_list[r]); });
-    m_node_list = node_list;
+    m_ref_node_list = RefNodeList{ref_edge_list.refId(), node_list, ref_edge_list.isBoundary()};
   }
+
+  // This is quite dirty but it allows a non negligible performance
+  // improvement
+  const_cast<Connectivity<Dimension>&>(mesh.connectivity()).addRefItemList(m_ref_node_list);
 }
 
 template <size_t Dimension>
 MeshNodeBoundary<Dimension>::MeshNodeBoundary(const Mesh<Connectivity<Dimension>>&, const RefNodeList& ref_node_list)
-  : m_node_list(ref_node_list.list()), m_boundary_name(ref_node_list.refId().tagName())
-{}
+  : m_ref_node_list(ref_node_list)
+{
+  if (not ref_node_list.isBoundary()) {
+    std::ostringstream ost;
+    ost << "invalid boundary \"" << rang::fgB::yellow << this->m_ref_node_list.refId() << rang::style::reset
+        << "\": inner nodes cannot be used to define mesh boundaries";
+    throw NormalError(ost.str());
+  }
+}
 
 template MeshNodeBoundary<1>::MeshNodeBoundary(const Mesh<Connectivity<1>>&, const RefFaceList&);
 template MeshNodeBoundary<2>::MeshNodeBoundary(const Mesh<Connectivity<2>>&, const RefFaceList&);
@@ -345,7 +315,7 @@ getMeshNodeBoundary(const Mesh<Connectivity<Dimension>>& mesh, const IBoundaryDe
   }
 
   std::ostringstream ost;
-  ost << "cannot find surface with name " << rang::fgB::red << boundary_descriptor << rang::style::reset;
+  ost << "cannot find node list with name " << rang::fgB::red << boundary_descriptor << rang::style::reset;
 
   throw NormalError(ost.str());
 }
diff --git a/src/mesh/MeshNodeBoundary.hpp b/src/mesh/MeshNodeBoundary.hpp
index 0422118ffc14abbc85030d4d56ca2f2da29eadb9..ce56579e853171e4b5be0d1f195936a75b838f78 100644
--- a/src/mesh/MeshNodeBoundary.hpp
+++ b/src/mesh/MeshNodeBoundary.hpp
@@ -16,8 +16,7 @@ template <size_t Dimension>
 class [[nodiscard]] MeshNodeBoundary   // clazy:exclude=copyable-polymorphic
 {
  protected:
-  Array<const NodeId> m_node_list;
-  std::string m_boundary_name;
+  RefNodeList m_ref_node_list;
 
   std::array<TinyVector<Dimension>, Dimension*(Dimension - 1)> _getBounds(const Mesh<Connectivity<Dimension>>& mesh)
     const;
@@ -30,9 +29,16 @@ class [[nodiscard]] MeshNodeBoundary   // clazy:exclude=copyable-polymorphic
   MeshNodeBoundary& operator=(const MeshNodeBoundary&) = default;
   MeshNodeBoundary& operator=(MeshNodeBoundary&&) = default;
 
+  PUGS_INLINE
+  const RefNodeList& refNodeList() const
+  {
+    return m_ref_node_list;
+  }
+
+  PUGS_INLINE
   const Array<const NodeId>& nodeList() const
   {
-    return m_node_list;
+    return m_ref_node_list.list();
   }
 
  protected:
diff --git a/src/mesh/MeshRandomizer.cpp b/src/mesh/MeshRandomizer.cpp
index 34087ff69d78f1c2d2bfc83589350c376d5fea70..06028bab1c002c1898bed0e94d3e3d091d1db15c 100644
--- a/src/mesh/MeshRandomizer.cpp
+++ b/src/mesh/MeshRandomizer.cpp
@@ -96,17 +96,21 @@ class MeshRandomizerHandler::MeshRandomizer
               });
 
           } else if constexpr (std::is_same_v<BCType, AxisBoundaryCondition>) {
-            const Rd& t = bc.direction();
+            if constexpr (Dimension > 1) {
+              const Rd& t = bc.direction();
 
-            const Rdxd txt = tensorProduct(t, t);
+              const Rdxd txt = tensorProduct(t, t);
 
-            const Array<const NodeId>& node_list = bc.nodeList();
-            parallel_for(
-              node_list.size(), PUGS_LAMBDA(const size_t i_node) {
-                const NodeId node_id = node_list[i_node];
+              const Array<const NodeId>& node_list = bc.nodeList();
+              parallel_for(
+                node_list.size(), PUGS_LAMBDA(const size_t i_node) {
+                  const NodeId node_id = node_list[i_node];
 
-                shift[node_id] = txt * shift[node_id];
-              });
+                  shift[node_id] = txt * shift[node_id];
+                });
+            } else {
+              throw UnexpectedError("AxisBoundaryCondition make no sense in dimension 1");
+            }
 
           } else if constexpr (std::is_same_v<BCType, FixedBoundaryCondition>) {
             const Array<const NodeId>& node_list = bc.nodeList();
@@ -300,6 +304,14 @@ class MeshRandomizerHandler::MeshRandomizer<Dimension>::AxisBoundaryCondition
   ~AxisBoundaryCondition() = default;
 };
 
+template <>
+class MeshRandomizerHandler::MeshRandomizer<1>::AxisBoundaryCondition
+{
+ public:
+  AxisBoundaryCondition()  = default;
+  ~AxisBoundaryCondition() = default;
+};
+
 template <size_t Dimension>
 class MeshRandomizerHandler::MeshRandomizer<Dimension>::FixedBoundaryCondition
 {
diff --git a/src/mesh/RefItemList.hpp b/src/mesh/RefItemList.hpp
index 594b5ad25dc4a9769cd1d4645202cceeec6273e6..326e3278c948baae0ad8dd4d562399563a6f993a 100644
--- a/src/mesh/RefItemList.hpp
+++ b/src/mesh/RefItemList.hpp
@@ -15,6 +15,7 @@ class RefItemList
  private:
   RefId m_ref_id;
   Array<const ItemId> m_item_id_list;
+  bool m_is_boundary;
 
  public:
   const RefId&
@@ -29,8 +30,14 @@ class RefItemList
     return m_item_id_list;
   }
 
-  RefItemList(const RefId& ref_id, const Array<const ItemId>& item_id_list)
-    : m_ref_id(ref_id), m_item_id_list(item_id_list)
+  bool
+  isBoundary() const
+  {
+    return m_is_boundary;
+  }
+
+  RefItemList(const RefId& ref_id, const Array<const ItemId>& item_id_list, bool is_boundary)
+    : m_ref_id{ref_id}, m_item_id_list{item_id_list}, m_is_boundary{is_boundary}
   {
     ;
   }
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 52a41bcb69fc8181fce73b92059b49cf1cccf314..196938400cab3735d0633b1d1ba41efc00358f4b 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -175,6 +175,15 @@ add_executable (mpi_unit_tests
   test_ItemArrayUtils.cpp
   test_ItemValue.cpp
   test_ItemValueUtils.cpp
+  test_MeshEdgeBoundary.cpp
+  test_MeshFaceBoundary.cpp
+  test_MeshFlatEdgeBoundary.cpp
+  test_MeshFlatFaceBoundary.cpp
+  test_MeshFlatNodeBoundary.cpp
+  test_MeshLineEdgeBoundary.cpp
+  test_MeshLineFaceBoundary.cpp
+  test_MeshLineNodeBoundary.cpp
+  test_MeshNodeBoundary.cpp
   test_Messenger.cpp
   test_OFStream.cpp
   test_Partitioner.cpp
diff --git a/tests/test_MeshEdgeBoundary.cpp b/tests/test_MeshEdgeBoundary.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..32c1d1c6e33c2c109b5c18f67172e3f15136f0f9
--- /dev/null
+++ b/tests/test_MeshEdgeBoundary.cpp
@@ -0,0 +1,335 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+
+#include <mesh/Connectivity.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshEdgeBoundary.hpp>
+#include <mesh/NamedBoundaryDescriptor.hpp>
+#include <mesh/NumberedBoundaryDescriptor.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("MeshEdgeBoundary", "[mesh]")
+{
+  auto is_same = [](const auto& a, const auto& b) -> bool {
+    if (a.size() > 0 and b.size() > 0) {
+      return (a.size() == b.size()) and (&(a[0]) == &(b[0]));
+    } else {
+      return (a.size() == b.size());
+    }
+  };
+
+  auto get_edge_list_from_tag = [](const size_t tag, const auto& connectivity) -> Array<const EdgeId> {
+    for (size_t i = 0; i < connectivity.template numberOfRefItemList<ItemType::edge>(); ++i) {
+      const auto& ref_edge_list = connectivity.template refItemList<ItemType::edge>(i);
+      const RefId ref_id        = ref_edge_list.refId();
+      if (ref_id.tagNumber() == tag) {
+        return ref_edge_list.list();
+      }
+    }
+    return {};
+  };
+
+  auto get_edge_list_from_name = [](const std::string& name, const auto& connectivity) -> Array<const EdgeId> {
+    for (size_t i = 0; i < connectivity.template numberOfRefItemList<ItemType::edge>(); ++i) {
+      const auto& ref_edge_list = connectivity.template refItemList<ItemType::edge>(i);
+      const RefId ref_id        = ref_edge_list.refId();
+      if (ref_id.tagName() == name) {
+        return ref_edge_list.list();
+      }
+    }
+    return {};
+  };
+
+  SECTION("1D")
+  {
+    static constexpr size_t Dimension = 1;
+
+    using ConnectivityType = Connectivity<Dimension>;
+    using MeshType         = Mesh<ConnectivityType>;
+
+    SECTION("cartesian 1d")
+    {
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian1DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      const ConnectivityType& connectivity = mesh.connectivity();
+
+      {
+        const std::set<size_t> tag_set = {0, 1};
+
+        for (auto tag : tag_set) {
+          NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+          const auto& edge_boundary = getMeshEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto edge_list = get_edge_list_from_tag(tag, connectivity);
+          REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+        }
+      }
+
+      {
+        const std::set<std::string> name_set = {"XMIN", "XMAX"};
+
+        for (auto name : name_set) {
+          NamedBoundaryDescriptor named_boundary_descriptor(name);
+          const auto& edge_boundary = getMeshEdgeBoundary(mesh, named_boundary_descriptor);
+
+          auto edge_list = get_edge_list_from_name(name, connectivity);
+
+          REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+        }
+      }
+    }
+
+    SECTION("unordered 1d")
+    {
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().unordered1DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      const ConnectivityType& connectivity = mesh.connectivity();
+
+      {
+        const std::set<size_t> tag_set = {1, 2};
+
+        for (auto tag : tag_set) {
+          NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+          const auto& edge_boundary = getMeshEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto edge_list = get_edge_list_from_tag(tag, connectivity);
+          REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+        }
+      }
+
+      {
+        const std::set<std::string> name_set = {"XMIN", "XMAX"};
+
+        for (auto name : name_set) {
+          NamedBoundaryDescriptor named_boundary_descriptor(name);
+          const auto& edge_boundary = getMeshEdgeBoundary(mesh, named_boundary_descriptor);
+
+          auto edge_list = get_edge_list_from_name(name, connectivity);
+          REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+        }
+      }
+    }
+  }
+
+  SECTION("2D")
+  {
+    static constexpr size_t Dimension = 2;
+
+    using ConnectivityType = Connectivity<Dimension>;
+    using MeshType         = Mesh<ConnectivityType>;
+
+    SECTION("cartesian 2d")
+    {
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian2DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      const ConnectivityType& connectivity = mesh.connectivity();
+
+      {
+        const std::set<size_t> tag_set = {0, 1, 2, 3};
+
+        for (auto tag : tag_set) {
+          NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+          const auto& edge_boundary = getMeshEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto edge_list = get_edge_list_from_tag(tag, connectivity);
+          REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+        }
+      }
+
+      {
+        const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+        for (auto name : name_set) {
+          NamedBoundaryDescriptor numbered_boundary_descriptor(name);
+          const auto& edge_boundary = getMeshEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto edge_list = get_edge_list_from_name(name, connectivity);
+          REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+        }
+      }
+    }
+
+    SECTION("hybrid 2d")
+    {
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      const ConnectivityType& connectivity = mesh.connectivity();
+
+      {
+        const std::set<size_t> tag_set = {1, 2, 3, 4};
+
+        for (auto tag : tag_set) {
+          NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+          const auto& edge_boundary = getMeshEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto edge_list = get_edge_list_from_tag(tag, connectivity);
+          REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+        }
+      }
+
+      {
+        const std::set<std::string> name_set = {"XMIN", "YMIN", "XMAX", "YMIN"};
+
+        for (auto name : name_set) {
+          NamedBoundaryDescriptor numbered_boundary_descriptor(name);
+          const auto& edge_boundary = getMeshEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto edge_list = get_edge_list_from_name(name, connectivity);
+          REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+        }
+      }
+    }
+  }
+
+  SECTION("3D")
+  {
+    static constexpr size_t Dimension = 3;
+
+    using ConnectivityType = Connectivity<Dimension>;
+    using MeshType         = Mesh<ConnectivityType>;
+
+    SECTION("cartesian 3d")
+    {
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian3DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      const ConnectivityType& connectivity = mesh.connectivity();
+
+      {
+        const std::set<size_t> tag_set = {0, 1, 2, 3, 4, 5};
+
+        for (auto tag : tag_set) {
+          NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+          const auto& edge_boundary = getMeshEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto edge_list = get_edge_list_from_tag(tag, connectivity);
+          REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+        }
+      }
+
+      {
+        const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX"};
+
+        for (auto name : name_set) {
+          NamedBoundaryDescriptor numbered_boundary_descriptor(name);
+          const auto& edge_boundary = getMeshEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto edge_list = get_edge_list_from_name(name, connectivity);
+          REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+        }
+      }
+    }
+
+    SECTION("hybrid 3d")
+    {
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      const ConnectivityType& connectivity = mesh.connectivity();
+
+      {
+        const std::set<size_t> tag_set = {22, 23, 24, 25, 26, 27};
+
+        for (auto tag : tag_set) {
+          NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+          const auto& edge_boundary = getMeshEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto edge_list = get_edge_list_from_tag(tag, connectivity);
+          REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+        }
+      }
+
+      {
+        const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX"};
+
+        for (auto name : name_set) {
+          NamedBoundaryDescriptor numbered_boundary_descriptor(name);
+          const auto& edge_boundary = getMeshEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto edge_list = get_edge_list_from_name(name, connectivity);
+          REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+        }
+      }
+    }
+  }
+
+  SECTION("errors")
+  {
+    SECTION("cannot find boundary")
+    {
+      static constexpr size_t Dimension = 3;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      NamedBoundaryDescriptor named_boundary_descriptor("invalid_boundary");
+
+      REQUIRE_THROWS_WITH(getMeshEdgeBoundary(mesh, named_boundary_descriptor),
+                          "error: cannot find edge list with name \"invalid_boundary\"");
+    }
+
+    SECTION("suredge is inside")
+    {
+      SECTION("1D")
+      {
+        static constexpr size_t Dimension = 1;
+
+        using ConnectivityType = Connectivity<Dimension>;
+        using MeshType         = Mesh<ConnectivityType>;
+
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().unordered1DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        NamedBoundaryDescriptor named_boundary_descriptor("INTERFACE");
+
+        REQUIRE_THROWS_WITH(getMeshEdgeBoundary(mesh, named_boundary_descriptor),
+                            "error: invalid boundary \"INTERFACE\": inner edges cannot be used to define mesh "
+                            "boundaries");
+      }
+
+      SECTION("2D")
+      {
+        static constexpr size_t Dimension = 2;
+
+        using ConnectivityType = Connectivity<Dimension>;
+        using MeshType         = Mesh<ConnectivityType>;
+
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        NamedBoundaryDescriptor named_boundary_descriptor("INTERFACE");
+
+        REQUIRE_THROWS_WITH(getMeshEdgeBoundary(mesh, named_boundary_descriptor),
+                            "error: invalid boundary \"INTERFACE\": inner edges cannot be used to define mesh "
+                            "boundaries");
+      }
+
+      SECTION("3D")
+      {
+        static constexpr size_t Dimension = 3;
+
+        using ConnectivityType = Connectivity<Dimension>;
+        using MeshType         = Mesh<ConnectivityType>;
+
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        NamedBoundaryDescriptor named_boundary_descriptor("INTERFACE1");
+
+        REQUIRE_THROWS_WITH(getMeshEdgeBoundary(mesh, named_boundary_descriptor),
+                            "error: invalid boundary \"INTERFACE1\": inner edges cannot be used to define mesh "
+                            "boundaries");
+      }
+    }
+  }
+}
diff --git a/tests/test_MeshFaceBoundary.cpp b/tests/test_MeshFaceBoundary.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..472427aaf0c7f9c85f566dd610020e91759aad84
--- /dev/null
+++ b/tests/test_MeshFaceBoundary.cpp
@@ -0,0 +1,335 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+
+#include <mesh/Connectivity.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshFaceBoundary.hpp>
+#include <mesh/NamedBoundaryDescriptor.hpp>
+#include <mesh/NumberedBoundaryDescriptor.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("MeshFaceBoundary", "[mesh]")
+{
+  auto is_same = [](const auto& a, const auto& b) -> bool {
+    if (a.size() > 0 and b.size() > 0) {
+      return (a.size() == b.size()) and (&(a[0]) == &(b[0]));
+    } else {
+      return (a.size() == b.size());
+    }
+  };
+
+  auto get_face_list_from_tag = [](const size_t tag, const auto& connectivity) -> Array<const FaceId> {
+    for (size_t i = 0; i < connectivity.template numberOfRefItemList<ItemType::face>(); ++i) {
+      const auto& ref_face_list = connectivity.template refItemList<ItemType::face>(i);
+      const RefId ref_id        = ref_face_list.refId();
+      if (ref_id.tagNumber() == tag) {
+        return ref_face_list.list();
+      }
+    }
+    return {};
+  };
+
+  auto get_face_list_from_name = [](const std::string& name, const auto& connectivity) -> Array<const FaceId> {
+    for (size_t i = 0; i < connectivity.template numberOfRefItemList<ItemType::face>(); ++i) {
+      const auto& ref_face_list = connectivity.template refItemList<ItemType::face>(i);
+      const RefId ref_id        = ref_face_list.refId();
+      if (ref_id.tagName() == name) {
+        return ref_face_list.list();
+      }
+    }
+    return {};
+  };
+
+  SECTION("1D")
+  {
+    static constexpr size_t Dimension = 1;
+
+    using ConnectivityType = Connectivity<Dimension>;
+    using MeshType         = Mesh<ConnectivityType>;
+
+    SECTION("cartesian 1d")
+    {
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian1DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      const ConnectivityType& connectivity = mesh.connectivity();
+
+      {
+        const std::set<size_t> tag_set = {0, 1};
+
+        for (auto tag : tag_set) {
+          NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+          const auto& face_boundary = getMeshFaceBoundary(mesh, numbered_boundary_descriptor);
+
+          auto face_list = get_face_list_from_tag(tag, connectivity);
+          REQUIRE(is_same(face_boundary.faceList(), face_list));
+        }
+      }
+
+      {
+        const std::set<std::string> name_set = {"XMIN", "XMAX"};
+
+        for (auto name : name_set) {
+          NamedBoundaryDescriptor named_boundary_descriptor(name);
+          const auto& face_boundary = getMeshFaceBoundary(mesh, named_boundary_descriptor);
+
+          auto face_list = get_face_list_from_name(name, connectivity);
+
+          REQUIRE(is_same(face_boundary.faceList(), face_list));
+        }
+      }
+    }
+
+    SECTION("unordered 1d")
+    {
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().unordered1DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      const ConnectivityType& connectivity = mesh.connectivity();
+
+      {
+        const std::set<size_t> tag_set = {1, 2};
+
+        for (auto tag : tag_set) {
+          NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+          const auto& face_boundary = getMeshFaceBoundary(mesh, numbered_boundary_descriptor);
+
+          auto face_list = get_face_list_from_tag(tag, connectivity);
+          REQUIRE(is_same(face_boundary.faceList(), face_list));
+        }
+      }
+
+      {
+        const std::set<std::string> name_set = {"XMIN", "XMAX"};
+
+        for (auto name : name_set) {
+          NamedBoundaryDescriptor named_boundary_descriptor(name);
+          const auto& face_boundary = getMeshFaceBoundary(mesh, named_boundary_descriptor);
+
+          auto face_list = get_face_list_from_name(name, connectivity);
+          REQUIRE(is_same(face_boundary.faceList(), face_list));
+        }
+      }
+    }
+  }
+
+  SECTION("2D")
+  {
+    static constexpr size_t Dimension = 2;
+
+    using ConnectivityType = Connectivity<Dimension>;
+    using MeshType         = Mesh<ConnectivityType>;
+
+    SECTION("cartesian 2d")
+    {
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian2DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      const ConnectivityType& connectivity = mesh.connectivity();
+
+      {
+        const std::set<size_t> tag_set = {0, 1, 2, 3};
+
+        for (auto tag : tag_set) {
+          NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+          const auto& face_boundary = getMeshFaceBoundary(mesh, numbered_boundary_descriptor);
+
+          auto face_list = get_face_list_from_tag(tag, connectivity);
+          REQUIRE(is_same(face_boundary.faceList(), face_list));
+        }
+      }
+
+      {
+        const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+        for (auto name : name_set) {
+          NamedBoundaryDescriptor numbered_boundary_descriptor(name);
+          const auto& face_boundary = getMeshFaceBoundary(mesh, numbered_boundary_descriptor);
+
+          auto face_list = get_face_list_from_name(name, connectivity);
+          REQUIRE(is_same(face_boundary.faceList(), face_list));
+        }
+      }
+    }
+
+    SECTION("hybrid 2d")
+    {
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      const ConnectivityType& connectivity = mesh.connectivity();
+
+      {
+        const std::set<size_t> tag_set = {1, 2, 3, 4};
+
+        for (auto tag : tag_set) {
+          NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+          const auto& face_boundary = getMeshFaceBoundary(mesh, numbered_boundary_descriptor);
+
+          auto face_list = get_face_list_from_tag(tag, connectivity);
+          REQUIRE(is_same(face_boundary.faceList(), face_list));
+        }
+      }
+
+      {
+        const std::set<std::string> name_set = {"XMIN", "YMIN", "XMAX", "YMIN"};
+
+        for (auto name : name_set) {
+          NamedBoundaryDescriptor numbered_boundary_descriptor(name);
+          const auto& face_boundary = getMeshFaceBoundary(mesh, numbered_boundary_descriptor);
+
+          auto face_list = get_face_list_from_name(name, connectivity);
+          REQUIRE(is_same(face_boundary.faceList(), face_list));
+        }
+      }
+    }
+  }
+
+  SECTION("3D")
+  {
+    static constexpr size_t Dimension = 3;
+
+    using ConnectivityType = Connectivity<Dimension>;
+    using MeshType         = Mesh<ConnectivityType>;
+
+    SECTION("cartesian 3d")
+    {
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian3DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      const ConnectivityType& connectivity = mesh.connectivity();
+
+      {
+        const std::set<size_t> tag_set = {0, 1, 2, 3, 4, 5};
+
+        for (auto tag : tag_set) {
+          NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+          const auto& face_boundary = getMeshFaceBoundary(mesh, numbered_boundary_descriptor);
+
+          auto face_list = get_face_list_from_tag(tag, connectivity);
+          REQUIRE(is_same(face_boundary.faceList(), face_list));
+        }
+      }
+
+      {
+        const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX"};
+
+        for (auto name : name_set) {
+          NamedBoundaryDescriptor numbered_boundary_descriptor(name);
+          const auto& face_boundary = getMeshFaceBoundary(mesh, numbered_boundary_descriptor);
+
+          auto face_list = get_face_list_from_name(name, connectivity);
+          REQUIRE(is_same(face_boundary.faceList(), face_list));
+        }
+      }
+    }
+
+    SECTION("hybrid 3d")
+    {
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      const ConnectivityType& connectivity = mesh.connectivity();
+
+      {
+        const std::set<size_t> tag_set = {22, 23, 24, 25, 26, 27};
+
+        for (auto tag : tag_set) {
+          NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+          const auto& face_boundary = getMeshFaceBoundary(mesh, numbered_boundary_descriptor);
+
+          auto face_list = get_face_list_from_tag(tag, connectivity);
+          REQUIRE(is_same(face_boundary.faceList(), face_list));
+        }
+      }
+
+      {
+        const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX"};
+
+        for (auto name : name_set) {
+          NamedBoundaryDescriptor numbered_boundary_descriptor(name);
+          const auto& face_boundary = getMeshFaceBoundary(mesh, numbered_boundary_descriptor);
+
+          auto face_list = get_face_list_from_name(name, connectivity);
+          REQUIRE(is_same(face_boundary.faceList(), face_list));
+        }
+      }
+    }
+  }
+
+  SECTION("errors")
+  {
+    SECTION("cannot find boundary")
+    {
+      static constexpr size_t Dimension = 3;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      NamedBoundaryDescriptor named_boundary_descriptor("invalid_boundary");
+
+      REQUIRE_THROWS_WITH(getMeshFaceBoundary(mesh, named_boundary_descriptor),
+                          "error: cannot find face list with name \"invalid_boundary\"");
+    }
+
+    SECTION("surface is inside")
+    {
+      SECTION("1D")
+      {
+        static constexpr size_t Dimension = 1;
+
+        using ConnectivityType = Connectivity<Dimension>;
+        using MeshType         = Mesh<ConnectivityType>;
+
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().unordered1DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        NamedBoundaryDescriptor named_boundary_descriptor("INTERFACE");
+
+        REQUIRE_THROWS_WITH(getMeshFaceBoundary(mesh, named_boundary_descriptor),
+                            "error: invalid boundary \"INTERFACE\": inner faces cannot be used to define mesh "
+                            "boundaries");
+      }
+
+      SECTION("2D")
+      {
+        static constexpr size_t Dimension = 2;
+
+        using ConnectivityType = Connectivity<Dimension>;
+        using MeshType         = Mesh<ConnectivityType>;
+
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        NamedBoundaryDescriptor named_boundary_descriptor("INTERFACE");
+
+        REQUIRE_THROWS_WITH(getMeshFaceBoundary(mesh, named_boundary_descriptor),
+                            "error: invalid boundary \"INTERFACE\": inner faces cannot be used to define mesh "
+                            "boundaries");
+      }
+
+      SECTION("3D")
+      {
+        static constexpr size_t Dimension = 3;
+
+        using ConnectivityType = Connectivity<Dimension>;
+        using MeshType         = Mesh<ConnectivityType>;
+
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        NamedBoundaryDescriptor named_boundary_descriptor("INTERFACE1");
+
+        REQUIRE_THROWS_WITH(getMeshFaceBoundary(mesh, named_boundary_descriptor),
+                            "error: invalid boundary \"INTERFACE1\": inner faces cannot be used to define mesh "
+                            "boundaries");
+      }
+    }
+  }
+}
diff --git a/tests/test_MeshFlatEdgeBoundary.cpp b/tests/test_MeshFlatEdgeBoundary.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..94700598238232b1e2a5e98692f57291f568dc39
--- /dev/null
+++ b/tests/test_MeshFlatEdgeBoundary.cpp
@@ -0,0 +1,1471 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+
+#include <algebra/TinyMatrix.hpp>
+#include <mesh/Connectivity.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshFlatEdgeBoundary.hpp>
+#include <mesh/NamedBoundaryDescriptor.hpp>
+#include <mesh/NumberedBoundaryDescriptor.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("MeshFlatEdgeBoundary", "[mesh]")
+{
+  auto is_same = [](const auto& a, const auto& b) -> bool {
+    if (a.size() > 0 and b.size() > 0) {
+      return (a[0] == b[0]);
+    } else {
+      return (a.size() == b.size());
+    }
+  };
+
+  auto get_edge_list_from_tag = [](const size_t tag, const auto& connectivity) -> Array<const EdgeId> {
+    for (size_t i = 0; i < connectivity.template numberOfRefItemList<ItemType::edge>(); ++i) {
+      const auto& ref_edge_list = connectivity.template refItemList<ItemType::edge>(i);
+      const RefId ref_id        = ref_edge_list.refId();
+      if (ref_id.tagNumber() == tag) {
+        return ref_edge_list.list();
+      }
+    }
+    return {};
+  };
+
+  auto get_edge_list_from_name = [](const std::string& name, const auto& connectivity) -> Array<const EdgeId> {
+    for (size_t i = 0; i < connectivity.template numberOfRefItemList<ItemType::edge>(); ++i) {
+      const auto& ref_edge_list = connectivity.template refItemList<ItemType::edge>(i);
+      const RefId ref_id        = ref_edge_list.refId();
+      if (ref_id.tagName() == name) {
+        return ref_edge_list.list();
+      }
+    }
+    return {};
+  };
+
+  SECTION("aligned axis")
+  {
+    SECTION("1D")
+    {
+      static constexpr size_t Dimension = 1;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R1 = TinyVector<1>;
+
+      SECTION("cartesian 1d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian1DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {0, 1};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R1 normal = zero;
+
+            switch (tag) {
+            case 0: {
+              normal = R1{-1};
+              break;
+            }
+            case 1: {
+              normal = R1{1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, named_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R1 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R1{-1};
+            } else if (name == "XMAX") {
+              normal = R1{1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+
+      SECTION("unordered 1d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().unordered1DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {1, 2};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R1 normal = zero;
+
+            switch (tag) {
+            case 1: {
+              normal = R1{-1};
+              break;
+            }
+            case 2: {
+              normal = R1{1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, named_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R1 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R1{-1};
+            } else if (name == "XMAX") {
+              normal = R1{1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+    }
+
+    SECTION("2D")
+    {
+      static constexpr size_t Dimension = 2;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R2 = TinyVector<2>;
+
+      SECTION("cartesian 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian2DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {0, 1, 2, 3};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R2 normal = zero;
+
+            switch (tag) {
+            case 0: {
+              normal = R2{-1, 0};
+              break;
+            }
+            case 1: {
+              normal = R2{1, 0};
+              break;
+            }
+            case 2: {
+              normal = R2{0, -1};
+              break;
+            }
+            case 3: {
+              normal = R2{0, 1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(10)),
+                                "error: cannot find edge list with name \"10\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(11)),
+                                "error: cannot find edge list with name \"11\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(12)),
+                                "error: cannot find edge list with name \"12\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(13)),
+                                "error: cannot find edge list with name \"13\"");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, named_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_name(name, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R2 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R2{-1, 0};
+            } else if (name == "XMAX") {
+              normal = R2{1, 0};
+            } else if (name == "YMIN") {
+              normal = R2{0, -1};
+            } else if (name == "YMAX") {
+              normal = R2{0, 1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMIN")),
+                                "error: cannot find edge list with name \"XMINYMIN\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAX")),
+                                "error: cannot find edge list with name \"XMINYMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMIN")),
+                                "error: cannot find edge list with name \"XMAXYMIN\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAX")),
+                                "error: cannot find edge list with name \"XMAXYMAX\"");
+          }
+        }
+      }
+
+      SECTION("hybrid 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {1, 2, 3, 4};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R2 normal = zero;
+
+            switch (tag) {
+            case 1: {
+              normal = R2{-1, 0};
+              break;
+            }
+            case 2: {
+              normal = R2{1, 0};
+              break;
+            }
+            case 3: {
+              normal = R2{0, 1};
+              break;
+            }
+            case 4: {
+              normal = R2{0, -1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(8)),
+                                "error: cannot find edge list with name \"8\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(9)),
+                                "error: cannot find edge list with name \"9\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(10)),
+                                "error: cannot find edge list with name \"10\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(11)),
+                                "error: cannot find edge list with name \"11\"");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, named_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_name(name, connectivity);
+
+            R2 normal = zero;
+
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            if (name == "XMIN") {
+              normal = R2{-1, 0};
+            } else if (name == "XMAX") {
+              normal = R2{1, 0};
+            } else if (name == "YMIN") {
+              normal = R2{0, -1};
+            } else if (name == "YMAX") {
+              normal = R2{0, 1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMIN")),
+                                "error: cannot find edge list with name \"XMINYMIN\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAX")),
+                                "error: cannot find edge list with name \"XMINYMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMIN")),
+                                "error: cannot find edge list with name \"XMAXYMIN\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAX")),
+                                "error: cannot find edge list with name \"XMAXYMAX\"");
+          }
+        }
+      }
+    }
+
+    SECTION("3D")
+    {
+      static constexpr size_t Dimension = 3;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R3 = TinyVector<3>;
+
+      SECTION("cartesian 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian3DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {0, 1, 2, 3, 4, 5};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R3 normal = zero;
+
+            switch (tag) {
+            case 0: {
+              normal = R3{-1, 0, 0};
+              break;
+            }
+            case 1: {
+              normal = R3{1, 0, 0};
+              break;
+            }
+            case 2: {
+              normal = R3{0, -1, 0};
+              break;
+            }
+            case 3: {
+              normal = R3{0, 1, 0};
+              break;
+            }
+            case 4: {
+              normal = R3{0, 0, -1};
+              break;
+            }
+            case 5: {
+              normal = R3{0, 0, 1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(20)),
+                                "error: cannot find surface with name \"20\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(21)),
+                                "error: cannot find surface with name \"21\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(22)),
+                                "error: cannot find surface with name \"22\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(23)),
+                                "error: cannot find surface with name \"23\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(24)),
+                                "error: cannot find surface with name \"24\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(25)),
+                                "error: cannot find surface with name \"25\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(26)),
+                                "error: cannot find surface with name \"26\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(27)),
+                                "error: cannot find surface with name \"27\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(28)),
+                                "error: cannot find surface with name \"28\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(29)),
+                                "error: cannot find surface with name \"29\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(30)),
+                                "error: cannot find surface with name \"30\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(31)),
+                                "error: cannot find surface with name \"31\"");
+
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(10)),
+                                "error: cannot find edge list with name \"10\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(11)),
+                                "error: cannot find edge list with name \"11\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(12)),
+                                "error: cannot find edge list with name \"12\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(13)),
+                                "error: cannot find edge list with name \"13\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(14)),
+                                "error: cannot find edge list with name \"14\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(15)),
+                                "error: cannot find edge list with name \"15\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(16)),
+                                "error: cannot find edge list with name \"16\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(17)),
+                                "error: cannot find edge list with name \"17\"");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, named_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R3 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R3{-1, 0, 0};
+            } else if (name == "XMAX") {
+              normal = R3{1, 0, 0};
+            } else if (name == "YMIN") {
+              normal = R3{0, -1, 0};
+            } else if (name == "YMAX") {
+              normal = R3{0, 1, 0};
+            } else if (name == "ZMIN") {
+              normal = R3{0, 0, -1};
+            } else if (name == "ZMAX") {
+              normal = R3{0, 0, 1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMIN")),
+                                "error: cannot find surface with name \"XMINYMIN\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAX")),
+                                "error: cannot find surface with name \"XMINYMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAX")),
+                                "error: cannot find surface with name \"XMAXYMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAX")),
+                                "error: cannot find surface with name \"XMINYMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINZMIN")),
+                                "error: cannot find surface with name \"XMINZMIN\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINZMAX")),
+                                "error: cannot find surface with name \"XMINZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXZMAX")),
+                                "error: cannot find surface with name \"XMAXZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINZMAX")),
+                                "error: cannot find surface with name \"XMINZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("YMINZMIN")),
+                                "error: cannot find surface with name \"YMINZMIN\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("YMINZMAX")),
+                                "error: cannot find surface with name \"YMINZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("YMAXZMAX")),
+                                "error: cannot find surface with name \"YMAXZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("YMINZMAX")),
+                                "error: cannot find surface with name \"YMINZMAX\"");
+
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMINZMIN")),
+                                "error: cannot find edge list with name \"XMINYMINZMIN\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMINZMIN")),
+                                "error: cannot find edge list with name \"XMAXYMINZMIN\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAXZMIN")),
+                                "error: cannot find edge list with name \"XMAXYMAXZMIN\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAXZMIN")),
+                                "error: cannot find edge list with name \"XMINYMAXZMIN\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMINZMAX")),
+                                "error: cannot find edge list with name \"XMINYMINZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMINZMAX")),
+                                "error: cannot find edge list with name \"XMAXYMINZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAXZMAX")),
+                                "error: cannot find edge list with name \"XMAXYMAXZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAXZMAX")),
+                                "error: cannot find edge list with name \"XMINYMAXZMAX\"");
+          }
+        }
+      }
+
+      SECTION("hybrid 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {22, 23, 24, 25, 26, 27};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R3 normal = zero;
+
+            switch (tag) {
+            case 22: {
+              normal = R3{-1, 0, 0};
+              break;
+            }
+            case 23: {
+              normal = R3{1, 0, 0};
+              break;
+            }
+            case 24: {
+              normal = R3{0, 0, 1};
+              break;
+            }
+            case 25: {
+              normal = R3{0, 0, -1};
+              break;
+            }
+            case 26: {
+              normal = R3{0, 1, 0};
+              break;
+            }
+            case 27: {
+              normal = R3{0, -1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(28)),
+                                "error: cannot find surface with name \"28\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(29)),
+                                "error: cannot find surface with name \"29\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(30)),
+                                "error: cannot find surface with name \"30\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(31)),
+                                "error: cannot find surface with name \"31\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(32)),
+                                "error: cannot find surface with name \"32\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(33)),
+                                "error: cannot find surface with name \"33\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(34)),
+                                "error: cannot find surface with name \"34\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(35)),
+                                "error: cannot find surface with name \"35\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(36)),
+                                "error: cannot find surface with name \"36\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(37)),
+                                "error: cannot find surface with name \"37\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(38)),
+                                "error: cannot find surface with name \"38\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(39)),
+                                "error: cannot find surface with name \"39\"");
+
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(40)),
+                                "error: cannot find edge list with name \"40\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(41)),
+                                "error: cannot find edge list with name \"41\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(42)),
+                                "error: cannot find edge list with name \"42\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(43)),
+                                "error: cannot find edge list with name \"43\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(44)),
+                                "error: cannot find edge list with name \"44\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(45)),
+                                "error: cannot find edge list with name \"45\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(47)),
+                                "error: cannot find edge list with name \"47\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NumberedBoundaryDescriptor(51)),
+                                "error: cannot find edge list with name \"51\"");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, named_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R3 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R3{-1, 0, 0};
+            } else if (name == "XMAX") {
+              normal = R3{1, 0, 0};
+            } else if (name == "YMIN") {
+              normal = R3{0, -1, 0};
+            } else if (name == "YMAX") {
+              normal = R3{0, 1, 0};
+            } else if (name == "ZMIN") {
+              normal = R3{0, 0, -1};
+            } else if (name == "ZMAX") {
+              normal = R3{0, 0, 1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMIN")),
+                                "error: cannot find surface with name \"XMINYMIN\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAX")),
+                                "error: cannot find surface with name \"XMINYMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAX")),
+                                "error: cannot find surface with name \"XMAXYMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAX")),
+                                "error: cannot find surface with name \"XMINYMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINZMIN")),
+                                "error: cannot find surface with name \"XMINZMIN\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINZMAX")),
+                                "error: cannot find surface with name \"XMINZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXZMAX")),
+                                "error: cannot find surface with name \"XMAXZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINZMAX")),
+                                "error: cannot find surface with name \"XMINZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("YMINZMIN")),
+                                "error: cannot find surface with name \"YMINZMIN\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("YMINZMAX")),
+                                "error: cannot find surface with name \"YMINZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("YMAXZMAX")),
+                                "error: cannot find surface with name \"YMAXZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("YMINZMAX")),
+                                "error: cannot find surface with name \"YMINZMAX\"");
+
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMINZMIN")),
+                                "error: cannot find edge list with name \"XMINYMINZMIN\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMINZMIN")),
+                                "error: cannot find edge list with name \"XMAXYMINZMIN\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAXZMIN")),
+                                "error: cannot find edge list with name \"XMAXYMAXZMIN\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAXZMIN")),
+                                "error: cannot find edge list with name \"XMINYMAXZMIN\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMINZMAX")),
+                                "error: cannot find edge list with name \"XMINYMINZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMINZMAX")),
+                                "error: cannot find edge list with name \"XMAXYMINZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAXZMAX")),
+                                "error: cannot find edge list with name \"XMAXYMAXZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAXZMAX")),
+                                "error: cannot find edge list with name \"XMINYMAXZMAX\"");
+          }
+        }
+      }
+    }
+  }
+
+  SECTION("rotated axis")
+  {
+    SECTION("2D")
+    {
+      static constexpr size_t Dimension = 2;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R2 = TinyVector<2>;
+
+      const double theta = 0.3;
+      const TinyMatrix<2> R{std::cos(theta), -std::sin(theta),   //
+                            std::sin(theta), std::cos(theta)};
+
+      SECTION("cartesian 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian2DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R2> rotated_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {0, 1, 2, 3};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R2 normal = zero;
+
+            switch (tag) {
+            case 0: {
+              normal = R * R2{-1, 0};
+              break;
+            }
+            case 1: {
+              normal = R * R2{1, 0};
+              break;
+            }
+            case 2: {
+              normal = R * R2{0, -1};
+              break;
+            }
+            case 3: {
+              normal = R * R2{0, 1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, named_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_name(name, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R2 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R * R2{-1, 0};
+            } else if (name == "XMAX") {
+              normal = R * R2{1, 0};
+            } else if (name == "YMIN") {
+              normal = R * R2{0, -1};
+            } else if (name == "YMAX") {
+              normal = R * R2{0, 1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+
+      SECTION("hybrid 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R2> rotated_xr{connectivity};
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {1, 2, 3, 4};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R2 normal = zero;
+
+            switch (tag) {
+            case 1: {
+              normal = R * R2{-1, 0};
+              break;
+            }
+            case 2: {
+              normal = R * R2{1, 0};
+              break;
+            }
+            case 3: {
+              normal = R * R2{0, 1};
+              break;
+            }
+            case 4: {
+              normal = R * R2{0, -1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, named_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_name(name, connectivity);
+
+            R2 normal = zero;
+
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            if (name == "XMIN") {
+              normal = R * R2{-1, 0};
+            } else if (name == "XMAX") {
+              normal = R * R2{1, 0};
+            } else if (name == "YMIN") {
+              normal = R * R2{0, -1};
+            } else if (name == "YMAX") {
+              normal = R * R2{0, 1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+    }
+
+    SECTION("3D")
+    {
+      static constexpr size_t Dimension = 3;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R3 = TinyVector<3>;
+
+      const double theta = 0.3;
+      const double phi   = 0.4;
+      const TinyMatrix<3> R =
+        TinyMatrix<3>{std::cos(theta), -std::sin(theta), 0, std::sin(theta), std::cos(theta), 0, 0, 0, 1} *
+        TinyMatrix<3>{0, std::cos(phi), -std::sin(phi), 0, std::sin(phi), std::cos(phi), 1, 0, 0};
+
+      SECTION("cartesian 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian3DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R3> rotated_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {0, 1, 2, 3, 4, 5};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R3 normal = zero;
+
+            switch (tag) {
+            case 0: {
+              normal = R * R3{-1, 0, 0};
+              break;
+            }
+            case 1: {
+              normal = R * R3{1, 0, 0};
+              break;
+            }
+            case 2: {
+              normal = R * R3{0, -1, 0};
+              break;
+            }
+            case 3: {
+              normal = R * R3{0, 1, 0};
+              break;
+            }
+            case 4: {
+              normal = R * R3{0, 0, -1};
+              break;
+            }
+            case 5: {
+              normal = R * R3{0, 0, 1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, named_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R3 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R * R3{-1, 0, 0};
+            } else if (name == "XMAX") {
+              normal = R * R3{1, 0, 0};
+            } else if (name == "YMIN") {
+              normal = R * R3{0, -1, 0};
+            } else if (name == "YMAX") {
+              normal = R * R3{0, 1, 0};
+            } else if (name == "ZMIN") {
+              normal = R * R3{0, 0, -1};
+            } else if (name == "ZMAX") {
+              normal = R * R3{0, 0, 1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+
+      SECTION("hybrid 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R3> rotated_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {22, 23, 24, 25, 26, 27};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R3 normal = zero;
+
+            switch (tag) {
+            case 22: {
+              normal = R * R3{-1, 0, 0};
+              break;
+            }
+            case 23: {
+              normal = R * R3{1, 0, 0};
+              break;
+            }
+            case 24: {
+              normal = R * R3{0, 0, 1};
+              break;
+            }
+            case 25: {
+              normal = R * R3{0, 0, -1};
+              break;
+            }
+            case 26: {
+              normal = R * R3{0, 1, 0};
+              break;
+            }
+            case 27: {
+              normal = R * R3{0, -1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, named_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R3 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R * R3{-1, 0, 0};
+            } else if (name == "XMAX") {
+              normal = R * R3{1, 0, 0};
+            } else if (name == "YMIN") {
+              normal = R * R3{0, -1, 0};
+            } else if (name == "YMAX") {
+              normal = R * R3{0, 1, 0};
+            } else if (name == "ZMIN") {
+              normal = R * R3{0, 0, -1};
+            } else if (name == "ZMAX") {
+              normal = R * R3{0, 0, 1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+    }
+  }
+
+  SECTION("curved mesh")
+  {
+    SECTION("2D")
+    {
+      static constexpr size_t Dimension = 2;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R2 = TinyVector<2>;
+
+      auto curve = [](const R2& X) -> R2 { return R2{X[0], (1 + X[0] * X[0]) * X[1]}; };
+
+      SECTION("hybrid 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<TinyVector<2>> curved_xr{connectivity};
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { curved_xr[node_id] = curve(xr[node_id]); });
+
+        MeshType mesh{p_mesh->shared_connectivity(), curved_xr};
+
+        {
+          const std::set<size_t> tag_set = {1, 2, 4};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R2 normal = zero;
+
+            switch (tag) {
+            case 1: {
+              normal = R2{-1, 0};
+              break;
+            }
+            case 2: {
+              normal = R2{1, 0};
+              break;
+            }
+            case 4: {
+              normal = R2{0, -1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(3);
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, numbered_boundary_descriptor),
+                                "error: invalid boundary \"YMAX(3)\": boundary is not flat!");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, named_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_name(name, connectivity);
+
+            R2 normal = zero;
+
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            if (name == "XMIN") {
+              normal = R2{-1, 0};
+            } else if (name == "XMAX") {
+              normal = R2{1, 0};
+            } else if (name == "YMIN") {
+              normal = R2{0, -1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NamedBoundaryDescriptor named_boundary_descriptor("YMAX");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, named_boundary_descriptor),
+                                "error: invalid boundary \"YMAX(3)\": boundary is not flat!");
+          }
+        }
+      }
+    }
+
+    SECTION("3D")
+    {
+      static constexpr size_t Dimension = 3;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R3 = TinyVector<3>;
+
+      SECTION("cartesian 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian3DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        auto curve = [](const R3& X) -> R3 {
+          return R3{X[0], (1 + X[0] * X[0]) * (X[1] + 1), (1 - 0.2 * X[0] * X[0]) * X[2]};
+        };
+
+        NodeValue<R3> curved_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { curved_xr[node_id] = curve(xr[node_id]); });
+
+        MeshType mesh{p_mesh->shared_connectivity(), curved_xr};
+
+        {
+          const std::set<size_t> tag_set = {0, 1, 2, 4};
+
+          for (auto tag : tag_set) {
+            auto edge_list = get_edge_list_from_tag(tag, connectivity);
+
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R3 normal = zero;
+
+            switch (tag) {
+            case 0: {
+              normal = R3{-1, 0, 0};
+              break;
+            }
+            case 1: {
+              normal = R3{1, 0, 0};
+              break;
+            }
+            case 2: {
+              normal = R3{0, -1, 0};
+              break;
+            }
+            case 4: {
+              normal = R3{0, 0, -1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(3);
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, numbered_boundary_descriptor),
+                                "error: invalid boundary \"YMAX(3)\": boundary is not flat!");
+          }
+
+          {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(5);
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, numbered_boundary_descriptor),
+                                "error: invalid boundary \"ZMAX(5)\": boundary is not flat!");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "ZMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, named_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R3 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R3{-1, 0, 0};
+            } else if (name == "XMAX") {
+              normal = R3{1, 0, 0};
+            } else if (name == "YMIN") {
+              normal = R3{0, -1, 0};
+            } else if (name == "ZMIN") {
+              normal = R3{0, 0, -1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NamedBoundaryDescriptor named_boundary_descriptor("YMAX");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, named_boundary_descriptor),
+                                "error: invalid boundary \"YMAX(3)\": boundary is not flat!");
+          }
+
+          {
+            NamedBoundaryDescriptor named_boundary_descriptor("ZMAX");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, named_boundary_descriptor),
+                                "error: invalid boundary \"ZMAX(5)\": boundary is not flat!");
+          }
+        }
+      }
+
+      SECTION("hybrid 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        auto curve = [](const R3& X) -> R3 {
+          return R3{X[0], (1 + X[0] * X[0]) * X[1], (1 - 0.2 * X[0] * X[0]) * X[2]};
+        };
+
+        NodeValue<R3> curved_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { curved_xr[node_id] = curve(xr[node_id]); });
+
+        MeshType mesh{p_mesh->shared_connectivity(), curved_xr};
+
+        {
+          const std::set<size_t> tag_set = {22, 23, 25, 27};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R3 normal = zero;
+
+            switch (tag) {
+            case 22: {
+              normal = R3{-1, 0, 0};
+              break;
+            }
+            case 23: {
+              normal = R3{1, 0, 0};
+              break;
+            }
+            case 25: {
+              normal = R3{0, 0, -1};
+              break;
+            }
+            case 27: {
+              normal = R3{0, -1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(24);
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, numbered_boundary_descriptor),
+                                "error: invalid boundary \"ZMAX(24)\": boundary is not flat!");
+          }
+
+          {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(26);
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, numbered_boundary_descriptor),
+                                "error: invalid boundary \"YMAX(26)\": boundary is not flat!");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "ZMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& edge_boundary = getMeshFlatEdgeBoundary(mesh, named_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R3 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R3{-1, 0, 0};
+            } else if (name == "XMAX") {
+              normal = R3{1, 0, 0};
+            } else if (name == "YMIN") {
+              normal = R3{0, -1, 0};
+            } else if (name == "ZMIN") {
+              normal = R3{0, 0, -1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(edge_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NamedBoundaryDescriptor named_boundary_descriptor("YMAX");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, named_boundary_descriptor),
+                                "error: invalid boundary \"YMAX(26)\": boundary is not flat!");
+          }
+
+          {
+            NamedBoundaryDescriptor named_boundary_descriptor("ZMAX");
+            REQUIRE_THROWS_WITH(getMeshFlatEdgeBoundary(mesh, named_boundary_descriptor),
+                                "error: invalid boundary \"ZMAX(24)\": boundary is not flat!");
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/tests/test_MeshFlatFaceBoundary.cpp b/tests/test_MeshFlatFaceBoundary.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e01d79d1dc456f6f193d42db86c4f1f5ce08b75f
--- /dev/null
+++ b/tests/test_MeshFlatFaceBoundary.cpp
@@ -0,0 +1,1251 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+
+#include <algebra/TinyMatrix.hpp>
+#include <mesh/Connectivity.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshFlatFaceBoundary.hpp>
+#include <mesh/NamedBoundaryDescriptor.hpp>
+#include <mesh/NumberedBoundaryDescriptor.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("MeshFlatFaceBoundary", "[mesh]")
+{
+  auto is_same = [](const auto& a, const auto& b) -> bool {
+    if (a.size() > 0 and b.size() > 0) {
+      return (a[0] == b[0]);
+    } else {
+      return (a.size() == b.size());
+    }
+  };
+
+  auto get_face_list_from_tag = [](const size_t tag, const auto& connectivity) -> Array<const FaceId> {
+    for (size_t i = 0; i < connectivity.template numberOfRefItemList<ItemType::face>(); ++i) {
+      const auto& ref_face_list = connectivity.template refItemList<ItemType::face>(i);
+      const RefId ref_id        = ref_face_list.refId();
+      if (ref_id.tagNumber() == tag) {
+        return ref_face_list.list();
+      }
+    }
+    return {};
+  };
+
+  auto get_face_list_from_name = [](const std::string& name, const auto& connectivity) -> Array<const FaceId> {
+    for (size_t i = 0; i < connectivity.template numberOfRefItemList<ItemType::face>(); ++i) {
+      const auto& ref_face_list = connectivity.template refItemList<ItemType::face>(i);
+      const RefId ref_id        = ref_face_list.refId();
+      if (ref_id.tagName() == name) {
+        return ref_face_list.list();
+      }
+    }
+    return {};
+  };
+
+  SECTION("aligned axis")
+  {
+    SECTION("1D")
+    {
+      static constexpr size_t Dimension = 1;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R1 = TinyVector<1>;
+
+      SECTION("cartesian 1d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian1DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {0, 1};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, numbered_boundary_descriptor);
+
+            auto face_list = get_face_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R1 normal = zero;
+
+            switch (tag) {
+            case 0: {
+              normal = R1{-1};
+              break;
+            }
+            case 1: {
+              normal = R1{1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, named_boundary_descriptor);
+
+            auto face_list = get_face_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R1 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R1{-1};
+            } else if (name == "XMAX") {
+              normal = R1{1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+
+      SECTION("unordered 1d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().unordered1DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {1, 2};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, numbered_boundary_descriptor);
+
+            auto face_list = get_face_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R1 normal = zero;
+
+            switch (tag) {
+            case 1: {
+              normal = R1{-1};
+              break;
+            }
+            case 2: {
+              normal = R1{1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, named_boundary_descriptor);
+
+            auto face_list = get_face_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R1 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R1{-1};
+            } else if (name == "XMAX") {
+              normal = R1{1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+    }
+
+    SECTION("2D")
+    {
+      static constexpr size_t Dimension = 2;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R2 = TinyVector<2>;
+
+      SECTION("cartesian 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian2DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {0, 1, 2, 3};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, numbered_boundary_descriptor);
+
+            auto face_list = get_face_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R2 normal = zero;
+
+            switch (tag) {
+            case 0: {
+              normal = R2{-1, 0};
+              break;
+            }
+            case 1: {
+              normal = R2{1, 0};
+              break;
+            }
+            case 2: {
+              normal = R2{0, -1};
+              break;
+            }
+            case 3: {
+              normal = R2{0, 1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, named_boundary_descriptor);
+
+            auto face_list = get_face_list_from_name(name, connectivity);
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R2 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R2{-1, 0};
+            } else if (name == "XMAX") {
+              normal = R2{1, 0};
+            } else if (name == "YMIN") {
+              normal = R2{0, -1};
+            } else if (name == "YMAX") {
+              normal = R2{0, 1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+
+      SECTION("hybrid 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {1, 2, 3, 4};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, numbered_boundary_descriptor);
+
+            auto face_list = get_face_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R2 normal = zero;
+
+            switch (tag) {
+            case 1: {
+              normal = R2{-1, 0};
+              break;
+            }
+            case 2: {
+              normal = R2{1, 0};
+              break;
+            }
+            case 3: {
+              normal = R2{0, 1};
+              break;
+            }
+            case 4: {
+              normal = R2{0, -1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, named_boundary_descriptor);
+
+            auto face_list = get_face_list_from_name(name, connectivity);
+
+            R2 normal = zero;
+
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            if (name == "XMIN") {
+              normal = R2{-1, 0};
+            } else if (name == "XMAX") {
+              normal = R2{1, 0};
+            } else if (name == "YMIN") {
+              normal = R2{0, -1};
+            } else if (name == "YMAX") {
+              normal = R2{0, 1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+    }
+
+    SECTION("3D")
+    {
+      static constexpr size_t Dimension = 3;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R3 = TinyVector<3>;
+
+      SECTION("cartesian 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian3DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {0, 1, 2, 3, 4, 5};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, numbered_boundary_descriptor);
+
+            auto face_list = get_face_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R3 normal = zero;
+
+            switch (tag) {
+            case 0: {
+              normal = R3{-1, 0, 0};
+              break;
+            }
+            case 1: {
+              normal = R3{1, 0, 0};
+              break;
+            }
+            case 2: {
+              normal = R3{0, -1, 0};
+              break;
+            }
+            case 3: {
+              normal = R3{0, 1, 0};
+              break;
+            }
+            case 4: {
+              normal = R3{0, 0, -1};
+              break;
+            }
+            case 5: {
+              normal = R3{0, 0, 1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, named_boundary_descriptor);
+
+            auto face_list = get_face_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R3 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R3{-1, 0, 0};
+            } else if (name == "XMAX") {
+              normal = R3{1, 0, 0};
+            } else if (name == "YMIN") {
+              normal = R3{0, -1, 0};
+            } else if (name == "YMAX") {
+              normal = R3{0, 1, 0};
+            } else if (name == "ZMIN") {
+              normal = R3{0, 0, -1};
+            } else if (name == "ZMAX") {
+              normal = R3{0, 0, 1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+
+      SECTION("hybrid 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {22, 23, 24, 25, 26, 27};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, numbered_boundary_descriptor);
+
+            auto face_list = get_face_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R3 normal = zero;
+
+            switch (tag) {
+            case 22: {
+              normal = R3{-1, 0, 0};
+              break;
+            }
+            case 23: {
+              normal = R3{1, 0, 0};
+              break;
+            }
+            case 24: {
+              normal = R3{0, 0, 1};
+              break;
+            }
+            case 25: {
+              normal = R3{0, 0, -1};
+              break;
+            }
+            case 26: {
+              normal = R3{0, 1, 0};
+              break;
+            }
+            case 27: {
+              normal = R3{0, -1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, named_boundary_descriptor);
+
+            auto face_list = get_face_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R3 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R3{-1, 0, 0};
+            } else if (name == "XMAX") {
+              normal = R3{1, 0, 0};
+            } else if (name == "YMIN") {
+              normal = R3{0, -1, 0};
+            } else if (name == "YMAX") {
+              normal = R3{0, 1, 0};
+            } else if (name == "ZMIN") {
+              normal = R3{0, 0, -1};
+            } else if (name == "ZMAX") {
+              normal = R3{0, 0, 1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+    }
+  }
+
+  SECTION("rotated axis")
+  {
+    SECTION("2D")
+    {
+      static constexpr size_t Dimension = 2;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R2 = TinyVector<2>;
+
+      const double theta = 0.3;
+      const TinyMatrix<2> R{std::cos(theta), -std::sin(theta),   //
+                            std::sin(theta), std::cos(theta)};
+
+      SECTION("cartesian 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian2DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R2> rotated_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {0, 1, 2, 3};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, numbered_boundary_descriptor);
+
+            auto face_list = get_face_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R2 normal = zero;
+
+            switch (tag) {
+            case 0: {
+              normal = R * R2{-1, 0};
+              break;
+            }
+            case 1: {
+              normal = R * R2{1, 0};
+              break;
+            }
+            case 2: {
+              normal = R * R2{0, -1};
+              break;
+            }
+            case 3: {
+              normal = R * R2{0, 1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, named_boundary_descriptor);
+
+            auto face_list = get_face_list_from_name(name, connectivity);
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R2 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R * R2{-1, 0};
+            } else if (name == "XMAX") {
+              normal = R * R2{1, 0};
+            } else if (name == "YMIN") {
+              normal = R * R2{0, -1};
+            } else if (name == "YMAX") {
+              normal = R * R2{0, 1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+
+      SECTION("hybrid 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R2> rotated_xr{connectivity};
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {1, 2, 3, 4};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, numbered_boundary_descriptor);
+
+            auto face_list = get_face_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R2 normal = zero;
+
+            switch (tag) {
+            case 1: {
+              normal = R * R2{-1, 0};
+              break;
+            }
+            case 2: {
+              normal = R * R2{1, 0};
+              break;
+            }
+            case 3: {
+              normal = R * R2{0, 1};
+              break;
+            }
+            case 4: {
+              normal = R * R2{0, -1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, named_boundary_descriptor);
+
+            auto face_list = get_face_list_from_name(name, connectivity);
+
+            R2 normal = zero;
+
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            if (name == "XMIN") {
+              normal = R * R2{-1, 0};
+            } else if (name == "XMAX") {
+              normal = R * R2{1, 0};
+            } else if (name == "YMIN") {
+              normal = R * R2{0, -1};
+            } else if (name == "YMAX") {
+              normal = R * R2{0, 1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+    }
+
+    SECTION("3D")
+    {
+      static constexpr size_t Dimension = 3;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R3 = TinyVector<3>;
+
+      const double theta = 0.3;
+      const double phi   = 0.4;
+      const TinyMatrix<3> R =
+        TinyMatrix<3>{std::cos(theta), -std::sin(theta), 0, std::sin(theta), std::cos(theta), 0, 0, 0, 1} *
+        TinyMatrix<3>{0, std::cos(phi), -std::sin(phi), 0, std::sin(phi), std::cos(phi), 1, 0, 0};
+
+      SECTION("cartesian 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian3DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R3> rotated_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {0, 1, 2, 3, 4, 5};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, numbered_boundary_descriptor);
+
+            auto face_list = get_face_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R3 normal = zero;
+
+            switch (tag) {
+            case 0: {
+              normal = R * R3{-1, 0, 0};
+              break;
+            }
+            case 1: {
+              normal = R * R3{1, 0, 0};
+              break;
+            }
+            case 2: {
+              normal = R * R3{0, -1, 0};
+              break;
+            }
+            case 3: {
+              normal = R * R3{0, 1, 0};
+              break;
+            }
+            case 4: {
+              normal = R * R3{0, 0, -1};
+              break;
+            }
+            case 5: {
+              normal = R * R3{0, 0, 1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, named_boundary_descriptor);
+
+            auto face_list = get_face_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R3 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R * R3{-1, 0, 0};
+            } else if (name == "XMAX") {
+              normal = R * R3{1, 0, 0};
+            } else if (name == "YMIN") {
+              normal = R * R3{0, -1, 0};
+            } else if (name == "YMAX") {
+              normal = R * R3{0, 1, 0};
+            } else if (name == "ZMIN") {
+              normal = R * R3{0, 0, -1};
+            } else if (name == "ZMAX") {
+              normal = R * R3{0, 0, 1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+
+      SECTION("hybrid 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R3> rotated_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {22, 23, 24, 25, 26, 27};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, numbered_boundary_descriptor);
+
+            auto face_list = get_face_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R3 normal = zero;
+
+            switch (tag) {
+            case 22: {
+              normal = R * R3{-1, 0, 0};
+              break;
+            }
+            case 23: {
+              normal = R * R3{1, 0, 0};
+              break;
+            }
+            case 24: {
+              normal = R * R3{0, 0, 1};
+              break;
+            }
+            case 25: {
+              normal = R * R3{0, 0, -1};
+              break;
+            }
+            case 26: {
+              normal = R * R3{0, 1, 0};
+              break;
+            }
+            case 27: {
+              normal = R * R3{0, -1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, named_boundary_descriptor);
+
+            auto face_list = get_face_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R3 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R * R3{-1, 0, 0};
+            } else if (name == "XMAX") {
+              normal = R * R3{1, 0, 0};
+            } else if (name == "YMIN") {
+              normal = R * R3{0, -1, 0};
+            } else if (name == "YMAX") {
+              normal = R * R3{0, 1, 0};
+            } else if (name == "ZMIN") {
+              normal = R * R3{0, 0, -1};
+            } else if (name == "ZMAX") {
+              normal = R * R3{0, 0, 1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+    }
+  }
+
+  SECTION("curved mesh")
+  {
+    SECTION("2D")
+    {
+      static constexpr size_t Dimension = 2;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R2 = TinyVector<2>;
+
+      auto curve = [](const R2& X) -> R2 { return R2{X[0], (1 + X[0] * X[0]) * X[1]}; };
+
+      SECTION("hybrid 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<TinyVector<2>> curved_xr{connectivity};
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { curved_xr[node_id] = curve(xr[node_id]); });
+
+        MeshType mesh{p_mesh->shared_connectivity(), curved_xr};
+
+        {
+          const std::set<size_t> tag_set = {1, 2, 4};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, numbered_boundary_descriptor);
+
+            auto face_list = get_face_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R2 normal = zero;
+
+            switch (tag) {
+            case 1: {
+              normal = R2{-1, 0};
+              break;
+            }
+            case 2: {
+              normal = R2{1, 0};
+              break;
+            }
+            case 4: {
+              normal = R2{0, -1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(3);
+            REQUIRE_THROWS_WITH(getMeshFlatFaceBoundary(mesh, numbered_boundary_descriptor),
+                                "error: invalid boundary \"YMAX(3)\": boundary is not flat!");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, named_boundary_descriptor);
+
+            auto face_list = get_face_list_from_name(name, connectivity);
+
+            R2 normal = zero;
+
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            if (name == "XMIN") {
+              normal = R2{-1, 0};
+            } else if (name == "XMAX") {
+              normal = R2{1, 0};
+            } else if (name == "YMIN") {
+              normal = R2{0, -1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NamedBoundaryDescriptor named_boundary_descriptor("YMAX");
+            REQUIRE_THROWS_WITH(getMeshFlatFaceBoundary(mesh, named_boundary_descriptor),
+                                "error: invalid boundary \"YMAX(3)\": boundary is not flat!");
+          }
+        }
+      }
+    }
+
+    SECTION("3D")
+    {
+      static constexpr size_t Dimension = 3;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R3 = TinyVector<3>;
+
+      SECTION("cartesian 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian3DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        auto curve = [](const R3& X) -> R3 {
+          return R3{X[0], (1 + X[0] * X[0]) * (X[1] + 1), (1 - 0.2 * X[0] * X[0]) * X[2]};
+        };
+
+        NodeValue<R3> curved_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { curved_xr[node_id] = curve(xr[node_id]); });
+
+        MeshType mesh{p_mesh->shared_connectivity(), curved_xr};
+
+        {
+          const std::set<size_t> tag_set = {0, 1, 2, 4};
+
+          for (auto tag : tag_set) {
+            auto face_list = get_face_list_from_tag(tag, connectivity);
+
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, numbered_boundary_descriptor);
+
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R3 normal = zero;
+
+            switch (tag) {
+            case 0: {
+              normal = R3{-1, 0, 0};
+              break;
+            }
+            case 1: {
+              normal = R3{1, 0, 0};
+              break;
+            }
+            case 2: {
+              normal = R3{0, -1, 0};
+              break;
+            }
+            case 4: {
+              normal = R3{0, 0, -1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(3);
+            REQUIRE_THROWS_WITH(getMeshFlatFaceBoundary(mesh, numbered_boundary_descriptor),
+                                "error: invalid boundary \"YMAX(3)\": boundary is not flat!");
+          }
+
+          {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(5);
+            REQUIRE_THROWS_WITH(getMeshFlatFaceBoundary(mesh, numbered_boundary_descriptor),
+                                "error: invalid boundary \"ZMAX(5)\": boundary is not flat!");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "ZMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, named_boundary_descriptor);
+
+            auto face_list = get_face_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R3 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R3{-1, 0, 0};
+            } else if (name == "XMAX") {
+              normal = R3{1, 0, 0};
+            } else if (name == "YMIN") {
+              normal = R3{0, -1, 0};
+            } else if (name == "ZMIN") {
+              normal = R3{0, 0, -1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NamedBoundaryDescriptor named_boundary_descriptor("YMAX");
+            REQUIRE_THROWS_WITH(getMeshFlatFaceBoundary(mesh, named_boundary_descriptor),
+                                "error: invalid boundary \"YMAX(3)\": boundary is not flat!");
+          }
+
+          {
+            NamedBoundaryDescriptor named_boundary_descriptor("ZMAX");
+            REQUIRE_THROWS_WITH(getMeshFlatFaceBoundary(mesh, named_boundary_descriptor),
+                                "error: invalid boundary \"ZMAX(5)\": boundary is not flat!");
+          }
+        }
+      }
+
+      SECTION("hybrid 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        auto curve = [](const R3& X) -> R3 {
+          return R3{X[0], (1 + X[0] * X[0]) * X[1], (1 - 0.2 * X[0] * X[0]) * X[2]};
+        };
+
+        NodeValue<R3> curved_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { curved_xr[node_id] = curve(xr[node_id]); });
+
+        MeshType mesh{p_mesh->shared_connectivity(), curved_xr};
+
+        {
+          const std::set<size_t> tag_set = {22, 23, 25, 27};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, numbered_boundary_descriptor);
+
+            auto face_list = get_face_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R3 normal = zero;
+
+            switch (tag) {
+            case 22: {
+              normal = R3{-1, 0, 0};
+              break;
+            }
+            case 23: {
+              normal = R3{1, 0, 0};
+              break;
+            }
+            case 25: {
+              normal = R3{0, 0, -1};
+              break;
+            }
+            case 27: {
+              normal = R3{0, -1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(24);
+            REQUIRE_THROWS_WITH(getMeshFlatFaceBoundary(mesh, numbered_boundary_descriptor),
+                                "error: invalid boundary \"ZMAX(24)\": boundary is not flat!");
+          }
+
+          {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(26);
+            REQUIRE_THROWS_WITH(getMeshFlatFaceBoundary(mesh, numbered_boundary_descriptor),
+                                "error: invalid boundary \"YMAX(26)\": boundary is not flat!");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "ZMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& face_boundary = getMeshFlatFaceBoundary(mesh, named_boundary_descriptor);
+
+            auto face_list = get_face_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R3 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R3{-1, 0, 0};
+            } else if (name == "XMAX") {
+              normal = R3{1, 0, 0};
+            } else if (name == "YMIN") {
+              normal = R3{0, -1, 0};
+            } else if (name == "ZMIN") {
+              normal = R3{0, 0, -1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(face_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NamedBoundaryDescriptor named_boundary_descriptor("YMAX");
+            REQUIRE_THROWS_WITH(getMeshFlatFaceBoundary(mesh, named_boundary_descriptor),
+                                "error: invalid boundary \"YMAX(26)\": boundary is not flat!");
+          }
+
+          {
+            NamedBoundaryDescriptor named_boundary_descriptor("ZMAX");
+            REQUIRE_THROWS_WITH(getMeshFlatFaceBoundary(mesh, named_boundary_descriptor),
+                                "error: invalid boundary \"ZMAX(24)\": boundary is not flat!");
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/tests/test_MeshFlatNodeBoundary.cpp b/tests/test_MeshFlatNodeBoundary.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..29b0110569a14a6612a8b50cc76bc892c476d1ad
--- /dev/null
+++ b/tests/test_MeshFlatNodeBoundary.cpp
@@ -0,0 +1,1471 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+
+#include <algebra/TinyMatrix.hpp>
+#include <mesh/Connectivity.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshFlatNodeBoundary.hpp>
+#include <mesh/NamedBoundaryDescriptor.hpp>
+#include <mesh/NumberedBoundaryDescriptor.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("MeshFlatNodeBoundary", "[mesh]")
+{
+  auto is_same = [](const auto& a, const auto& b) -> bool {
+    if (a.size() > 0 and b.size() > 0) {
+      return (a[0] == b[0]);
+    } else {
+      return (a.size() == b.size());
+    }
+  };
+
+  auto get_node_list_from_tag = [](const size_t tag, const auto& connectivity) -> Array<const NodeId> {
+    for (size_t i = 0; i < connectivity.template numberOfRefItemList<ItemType::node>(); ++i) {
+      const auto& ref_node_list = connectivity.template refItemList<ItemType::node>(i);
+      const RefId ref_id        = ref_node_list.refId();
+      if (ref_id.tagNumber() == tag) {
+        return ref_node_list.list();
+      }
+    }
+    return {};
+  };
+
+  auto get_node_list_from_name = [](const std::string& name, const auto& connectivity) -> Array<const NodeId> {
+    for (size_t i = 0; i < connectivity.template numberOfRefItemList<ItemType::node>(); ++i) {
+      const auto& ref_node_list = connectivity.template refItemList<ItemType::node>(i);
+      const RefId ref_id        = ref_node_list.refId();
+      if (ref_id.tagName() == name) {
+        return ref_node_list.list();
+      }
+    }
+    return {};
+  };
+
+  SECTION("aligned axis")
+  {
+    SECTION("1D")
+    {
+      static constexpr size_t Dimension = 1;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R1 = TinyVector<1>;
+
+      SECTION("cartesian 1d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian1DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {0, 1};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R1 normal = zero;
+
+            switch (tag) {
+            case 0: {
+              normal = R1{-1};
+              break;
+            }
+            case 1: {
+              normal = R1{1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R1 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R1{-1};
+            } else if (name == "XMAX") {
+              normal = R1{1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+
+      SECTION("unordered 1d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().unordered1DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {1, 2};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R1 normal = zero;
+
+            switch (tag) {
+            case 1: {
+              normal = R1{-1};
+              break;
+            }
+            case 2: {
+              normal = R1{1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R1 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R1{-1};
+            } else if (name == "XMAX") {
+              normal = R1{1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+    }
+
+    SECTION("2D")
+    {
+      static constexpr size_t Dimension = 2;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R2 = TinyVector<2>;
+
+      SECTION("cartesian 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian2DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {0, 1, 2, 3};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R2 normal = zero;
+
+            switch (tag) {
+            case 0: {
+              normal = R2{-1, 0};
+              break;
+            }
+            case 1: {
+              normal = R2{1, 0};
+              break;
+            }
+            case 2: {
+              normal = R2{0, -1};
+              break;
+            }
+            case 3: {
+              normal = R2{0, 1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(10)),
+                                "error: invalid boundary \"XMINYMIN(10)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(11)),
+                                "error: invalid boundary \"XMAXYMIN(11)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(12)),
+                                "error: invalid boundary \"XMAXYMAX(12)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(13)),
+                                "error: invalid boundary \"XMINYMAX(13)\": unable to compute normal");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R2 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R2{-1, 0};
+            } else if (name == "XMAX") {
+              normal = R2{1, 0};
+            } else if (name == "YMIN") {
+              normal = R2{0, -1};
+            } else if (name == "YMAX") {
+              normal = R2{0, 1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMIN")),
+                                "error: invalid boundary \"XMINYMIN(10)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAX")),
+                                "error: invalid boundary \"XMINYMAX(13)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMIN")),
+                                "error: invalid boundary \"XMAXYMIN(11)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAX")),
+                                "error: invalid boundary \"XMAXYMAX(12)\": unable to compute normal");
+          }
+        }
+      }
+
+      SECTION("hybrid 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {1, 2, 3, 4};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R2 normal = zero;
+
+            switch (tag) {
+            case 1: {
+              normal = R2{-1, 0};
+              break;
+            }
+            case 2: {
+              normal = R2{1, 0};
+              break;
+            }
+            case 3: {
+              normal = R2{0, 1};
+              break;
+            }
+            case 4: {
+              normal = R2{0, -1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(8)),
+                                "error: invalid boundary \"XMINYMIN(8)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(9)),
+                                "error: invalid boundary \"XMINYMAX(9)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(10)),
+                                "error: invalid boundary \"XMAXYMIN(10)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(11)),
+                                "error: invalid boundary \"XMAXYMAX(11)\": unable to compute normal");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            R2 normal = zero;
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            if (name == "XMIN") {
+              normal = R2{-1, 0};
+            } else if (name == "XMAX") {
+              normal = R2{1, 0};
+            } else if (name == "YMIN") {
+              normal = R2{0, -1};
+            } else if (name == "YMAX") {
+              normal = R2{0, 1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMIN")),
+                                "error: invalid boundary \"XMINYMIN(8)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAX")),
+                                "error: invalid boundary \"XMINYMAX(9)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMIN")),
+                                "error: invalid boundary \"XMAXYMIN(10)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAX")),
+                                "error: invalid boundary \"XMAXYMAX(11)\": unable to compute normal");
+          }
+        }
+      }
+    }
+
+    SECTION("3D")
+    {
+      static constexpr size_t Dimension = 3;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R3 = TinyVector<3>;
+
+      SECTION("cartesian 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian3DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {0, 1, 2, 3, 4, 5};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 normal = zero;
+
+            switch (tag) {
+            case 0: {
+              normal = R3{-1, 0, 0};
+              break;
+            }
+            case 1: {
+              normal = R3{1, 0, 0};
+              break;
+            }
+            case 2: {
+              normal = R3{0, -1, 0};
+              break;
+            }
+            case 3: {
+              normal = R3{0, 1, 0};
+              break;
+            }
+            case 4: {
+              normal = R3{0, 0, -1};
+              break;
+            }
+            case 5: {
+              normal = R3{0, 0, 1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(20)),
+                                "error: cannot find surface with name \"20\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(21)),
+                                "error: cannot find surface with name \"21\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(22)),
+                                "error: cannot find surface with name \"22\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(23)),
+                                "error: cannot find surface with name \"23\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(24)),
+                                "error: cannot find surface with name \"24\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(25)),
+                                "error: cannot find surface with name \"25\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(26)),
+                                "error: cannot find surface with name \"26\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(27)),
+                                "error: cannot find surface with name \"27\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(28)),
+                                "error: cannot find surface with name \"28\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(29)),
+                                "error: cannot find surface with name \"29\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(30)),
+                                "error: cannot find surface with name \"30\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(31)),
+                                "error: cannot find surface with name \"31\"");
+
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(10)),
+                                "error: invalid boundary \"XMINYMINZMIN(10)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(11)),
+                                "error: invalid boundary \"XMAXYMINZMIN(11)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(12)),
+                                "error: invalid boundary \"XMAXYMAXZMIN(12)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(13)),
+                                "error: invalid boundary \"XMINYMAXZMIN(13)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(14)),
+                                "error: invalid boundary \"XMINYMINZMAX(14)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(15)),
+                                "error: invalid boundary \"XMAXYMINZMAX(15)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(16)),
+                                "error: invalid boundary \"XMAXYMAXZMAX(16)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(17)),
+                                "error: invalid boundary \"XMINYMAXZMAX(17)\": unable to compute normal");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R3{-1, 0, 0};
+            } else if (name == "XMAX") {
+              normal = R3{1, 0, 0};
+            } else if (name == "YMIN") {
+              normal = R3{0, -1, 0};
+            } else if (name == "YMAX") {
+              normal = R3{0, 1, 0};
+            } else if (name == "ZMIN") {
+              normal = R3{0, 0, -1};
+            } else if (name == "ZMAX") {
+              normal = R3{0, 0, 1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMIN")),
+                                "error: cannot find surface with name \"XMINYMIN\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAX")),
+                                "error: cannot find surface with name \"XMINYMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAX")),
+                                "error: cannot find surface with name \"XMAXYMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAX")),
+                                "error: cannot find surface with name \"XMINYMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMINZMIN")),
+                                "error: cannot find surface with name \"XMINZMIN\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMINZMAX")),
+                                "error: cannot find surface with name \"XMINZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXZMAX")),
+                                "error: cannot find surface with name \"XMAXZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMINZMAX")),
+                                "error: cannot find surface with name \"XMINZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("YMINZMIN")),
+                                "error: cannot find surface with name \"YMINZMIN\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("YMINZMAX")),
+                                "error: cannot find surface with name \"YMINZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("YMAXZMAX")),
+                                "error: cannot find surface with name \"YMAXZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("YMINZMAX")),
+                                "error: cannot find surface with name \"YMINZMAX\"");
+
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMINZMIN")),
+                                "error: invalid boundary \"XMINYMINZMIN(10)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMINZMIN")),
+                                "error: invalid boundary \"XMAXYMINZMIN(11)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAXZMIN")),
+                                "error: invalid boundary \"XMAXYMAXZMIN(12)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAXZMIN")),
+                                "error: invalid boundary \"XMINYMAXZMIN(13)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMINZMAX")),
+                                "error: invalid boundary \"XMINYMINZMAX(14)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMINZMAX")),
+                                "error: invalid boundary \"XMAXYMINZMAX(15)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAXZMAX")),
+                                "error: invalid boundary \"XMAXYMAXZMAX(16)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAXZMAX")),
+                                "error: invalid boundary \"XMINYMAXZMAX(17)\": unable to compute normal");
+          }
+        }
+      }
+
+      SECTION("hybrid 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {22, 23, 24, 25, 26, 27};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 normal = zero;
+
+            switch (tag) {
+            case 22: {
+              normal = R3{-1, 0, 0};
+              break;
+            }
+            case 23: {
+              normal = R3{1, 0, 0};
+              break;
+            }
+            case 24: {
+              normal = R3{0, 0, 1};
+              break;
+            }
+            case 25: {
+              normal = R3{0, 0, -1};
+              break;
+            }
+            case 26: {
+              normal = R3{0, 1, 0};
+              break;
+            }
+            case 27: {
+              normal = R3{0, -1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(28)),
+                                "error: cannot find surface with name \"28\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(29)),
+                                "error: cannot find surface with name \"29\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(30)),
+                                "error: cannot find surface with name \"30\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(31)),
+                                "error: cannot find surface with name \"31\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(32)),
+                                "error: cannot find surface with name \"32\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(33)),
+                                "error: cannot find surface with name \"33\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(34)),
+                                "error: cannot find surface with name \"34\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(35)),
+                                "error: cannot find surface with name \"35\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(36)),
+                                "error: cannot find surface with name \"36\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(37)),
+                                "error: cannot find surface with name \"37\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(38)),
+                                "error: cannot find surface with name \"38\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(39)),
+                                "error: cannot find surface with name \"39\"");
+
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(40)),
+                                "error: invalid boundary \"XMINYMINZMIN(40)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(41)),
+                                "error: invalid boundary \"XMAXYMINZMIN(41)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(42)),
+                                "error: invalid boundary \"XMINYMAXZMIN(42)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(43)),
+                                "error: invalid boundary \"XMINYMAXZMAX(43)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(44)),
+                                "error: invalid boundary \"XMINYMINZMAX(44)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(45)),
+                                "error: invalid boundary \"XMAXYMINZMAX(45)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(47)),
+                                "error: invalid boundary \"XMAXYMAXZMAX(47)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NumberedBoundaryDescriptor(51)),
+                                "error: invalid boundary \"XMAXYMAXZMIN(51)\": unable to compute normal");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R3{-1, 0, 0};
+            } else if (name == "XMAX") {
+              normal = R3{1, 0, 0};
+            } else if (name == "YMIN") {
+              normal = R3{0, -1, 0};
+            } else if (name == "YMAX") {
+              normal = R3{0, 1, 0};
+            } else if (name == "ZMIN") {
+              normal = R3{0, 0, -1};
+            } else if (name == "ZMAX") {
+              normal = R3{0, 0, 1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMIN")),
+                                "error: cannot find surface with name \"XMINYMIN\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAX")),
+                                "error: cannot find surface with name \"XMINYMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAX")),
+                                "error: cannot find surface with name \"XMAXYMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAX")),
+                                "error: cannot find surface with name \"XMINYMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMINZMIN")),
+                                "error: cannot find surface with name \"XMINZMIN\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMINZMAX")),
+                                "error: cannot find surface with name \"XMINZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXZMAX")),
+                                "error: cannot find surface with name \"XMAXZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMINZMAX")),
+                                "error: cannot find surface with name \"XMINZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("YMINZMIN")),
+                                "error: cannot find surface with name \"YMINZMIN\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("YMINZMAX")),
+                                "error: cannot find surface with name \"YMINZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("YMAXZMAX")),
+                                "error: cannot find surface with name \"YMAXZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("YMINZMAX")),
+                                "error: cannot find surface with name \"YMINZMAX\"");
+
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMINZMIN")),
+                                "error: invalid boundary \"XMINYMINZMIN(40)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMINZMIN")),
+                                "error: invalid boundary \"XMAXYMINZMIN(41)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAXZMIN")),
+                                "error: invalid boundary \"XMAXYMAXZMIN(51)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAXZMIN")),
+                                "error: invalid boundary \"XMINYMAXZMIN(42)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMINZMAX")),
+                                "error: invalid boundary \"XMINYMINZMAX(44)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMINZMAX")),
+                                "error: invalid boundary \"XMAXYMINZMAX(45)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAXZMAX")),
+                                "error: invalid boundary \"XMAXYMAXZMAX(47)\": unable to compute normal");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAXZMAX")),
+                                "error: invalid boundary \"XMINYMAXZMAX(43)\": unable to compute normal");
+          }
+        }
+      }
+    }
+  }
+
+  SECTION("rotated axis")
+  {
+    SECTION("2D")
+    {
+      static constexpr size_t Dimension = 2;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R2 = TinyVector<2>;
+
+      const double theta = 0.3;
+      const TinyMatrix<2> R{std::cos(theta), -std::sin(theta),   //
+                            std::sin(theta), std::cos(theta)};
+
+      SECTION("cartesian 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian2DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R2> rotated_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {0, 1, 2, 3};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R2 normal = zero;
+
+            switch (tag) {
+            case 0: {
+              normal = R * R2{-1, 0};
+              break;
+            }
+            case 1: {
+              normal = R * R2{1, 0};
+              break;
+            }
+            case 2: {
+              normal = R * R2{0, -1};
+              break;
+            }
+            case 3: {
+              normal = R * R2{0, 1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R2 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R * R2{-1, 0};
+            } else if (name == "XMAX") {
+              normal = R * R2{1, 0};
+            } else if (name == "YMIN") {
+              normal = R * R2{0, -1};
+            } else if (name == "YMAX") {
+              normal = R * R2{0, 1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+
+      SECTION("hybrid 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R2> rotated_xr{connectivity};
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {1, 2, 3, 4};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R2 normal = zero;
+
+            switch (tag) {
+            case 1: {
+              normal = R * R2{-1, 0};
+              break;
+            }
+            case 2: {
+              normal = R * R2{1, 0};
+              break;
+            }
+            case 3: {
+              normal = R * R2{0, 1};
+              break;
+            }
+            case 4: {
+              normal = R * R2{0, -1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            R2 normal = zero;
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            if (name == "XMIN") {
+              normal = R * R2{-1, 0};
+            } else if (name == "XMAX") {
+              normal = R * R2{1, 0};
+            } else if (name == "YMIN") {
+              normal = R * R2{0, -1};
+            } else if (name == "YMAX") {
+              normal = R * R2{0, 1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+    }
+
+    SECTION("3D")
+    {
+      static constexpr size_t Dimension = 3;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R3 = TinyVector<3>;
+
+      const double theta = 0.3;
+      const double phi   = 0.4;
+      const TinyMatrix<3> R =
+        TinyMatrix<3>{std::cos(theta), -std::sin(theta), 0, std::sin(theta), std::cos(theta), 0, 0, 0, 1} *
+        TinyMatrix<3>{0, std::cos(phi), -std::sin(phi), 0, std::sin(phi), std::cos(phi), 1, 0, 0};
+
+      SECTION("cartesian 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian3DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R3> rotated_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {0, 1, 2, 3, 4, 5};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 normal = zero;
+
+            switch (tag) {
+            case 0: {
+              normal = R * R3{-1, 0, 0};
+              break;
+            }
+            case 1: {
+              normal = R * R3{1, 0, 0};
+              break;
+            }
+            case 2: {
+              normal = R * R3{0, -1, 0};
+              break;
+            }
+            case 3: {
+              normal = R * R3{0, 1, 0};
+              break;
+            }
+            case 4: {
+              normal = R * R3{0, 0, -1};
+              break;
+            }
+            case 5: {
+              normal = R * R3{0, 0, 1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R * R3{-1, 0, 0};
+            } else if (name == "XMAX") {
+              normal = R * R3{1, 0, 0};
+            } else if (name == "YMIN") {
+              normal = R * R3{0, -1, 0};
+            } else if (name == "YMAX") {
+              normal = R * R3{0, 1, 0};
+            } else if (name == "ZMIN") {
+              normal = R * R3{0, 0, -1};
+            } else if (name == "ZMAX") {
+              normal = R * R3{0, 0, 1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+
+      SECTION("hybrid 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R3> rotated_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {22, 23, 24, 25, 26, 27};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 normal = zero;
+
+            switch (tag) {
+            case 22: {
+              normal = R * R3{-1, 0, 0};
+              break;
+            }
+            case 23: {
+              normal = R * R3{1, 0, 0};
+              break;
+            }
+            case 24: {
+              normal = R * R3{0, 0, 1};
+              break;
+            }
+            case 25: {
+              normal = R * R3{0, 0, -1};
+              break;
+            }
+            case 26: {
+              normal = R * R3{0, 1, 0};
+              break;
+            }
+            case 27: {
+              normal = R * R3{0, -1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R * R3{-1, 0, 0};
+            } else if (name == "XMAX") {
+              normal = R * R3{1, 0, 0};
+            } else if (name == "YMIN") {
+              normal = R * R3{0, -1, 0};
+            } else if (name == "YMAX") {
+              normal = R * R3{0, 1, 0};
+            } else if (name == "ZMIN") {
+              normal = R * R3{0, 0, -1};
+            } else if (name == "ZMAX") {
+              normal = R * R3{0, 0, 1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+    }
+  }
+
+  SECTION("curved mesh")
+  {
+    SECTION("2D")
+    {
+      static constexpr size_t Dimension = 2;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R2 = TinyVector<2>;
+
+      auto curve = [](const R2& X) -> R2 { return R2{X[0], (1 + X[0] * X[0]) * X[1]}; };
+
+      SECTION("hybrid 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<TinyVector<2>> curved_xr{connectivity};
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { curved_xr[node_id] = curve(xr[node_id]); });
+
+        MeshType mesh{p_mesh->shared_connectivity(), curved_xr};
+
+        {
+          const std::set<size_t> tag_set = {1, 2, 4};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R2 normal = zero;
+
+            switch (tag) {
+            case 1: {
+              normal = R2{-1, 0};
+              break;
+            }
+            case 2: {
+              normal = R2{1, 0};
+              break;
+            }
+            case 4: {
+              normal = R2{0, -1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(3);
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, numbered_boundary_descriptor),
+                                "error: invalid boundary \"YMAX(3)\": boundary is not flat!");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            R2 normal = zero;
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            if (name == "XMIN") {
+              normal = R2{-1, 0};
+            } else if (name == "XMAX") {
+              normal = R2{1, 0};
+            } else if (name == "YMIN") {
+              normal = R2{0, -1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NamedBoundaryDescriptor named_boundary_descriptor("YMAX");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, named_boundary_descriptor),
+                                "error: invalid boundary \"YMAX(3)\": boundary is not flat!");
+          }
+        }
+      }
+    }
+
+    SECTION("3D")
+    {
+      static constexpr size_t Dimension = 3;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R3 = TinyVector<3>;
+
+      SECTION("cartesian 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian3DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        auto curve = [](const R3& X) -> R3 {
+          return R3{X[0], (1 + X[0] * X[0]) * (X[1] + 1), (1 - 0.2 * X[0] * X[0]) * X[2]};
+        };
+
+        NodeValue<R3> curved_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { curved_xr[node_id] = curve(xr[node_id]); });
+
+        MeshType mesh{p_mesh->shared_connectivity(), curved_xr};
+
+        {
+          const std::set<size_t> tag_set = {0, 1, 2, 4};
+
+          for (auto tag : tag_set) {
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 normal = zero;
+
+            switch (tag) {
+            case 0: {
+              normal = R3{-1, 0, 0};
+              break;
+            }
+            case 1: {
+              normal = R3{1, 0, 0};
+              break;
+            }
+            case 2: {
+              normal = R3{0, -1, 0};
+              break;
+            }
+            case 4: {
+              normal = R3{0, 0, -1};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(3);
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, numbered_boundary_descriptor),
+                                "error: invalid boundary \"YMAX(3)\": boundary is not flat!");
+          }
+
+          {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(5);
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, numbered_boundary_descriptor),
+                                "error: invalid boundary \"ZMAX(5)\": boundary is not flat!");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "ZMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R3{-1, 0, 0};
+            } else if (name == "XMAX") {
+              normal = R3{1, 0, 0};
+            } else if (name == "YMIN") {
+              normal = R3{0, -1, 0};
+            } else if (name == "ZMIN") {
+              normal = R3{0, 0, -1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NamedBoundaryDescriptor named_boundary_descriptor("YMAX");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, named_boundary_descriptor),
+                                "error: invalid boundary \"YMAX(3)\": boundary is not flat!");
+          }
+
+          {
+            NamedBoundaryDescriptor named_boundary_descriptor("ZMAX");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, named_boundary_descriptor),
+                                "error: invalid boundary \"ZMAX(5)\": boundary is not flat!");
+          }
+        }
+      }
+
+      SECTION("hybrid 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        auto curve = [](const R3& X) -> R3 {
+          return R3{X[0], (1 + X[0] * X[0]) * X[1], (1 - 0.2 * X[0] * X[0]) * X[2]};
+        };
+
+        NodeValue<R3> curved_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { curved_xr[node_id] = curve(xr[node_id]); });
+
+        MeshType mesh{p_mesh->shared_connectivity(), curved_xr};
+
+        {
+          const std::set<size_t> tag_set = {22, 23, 25, 27};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 normal = zero;
+
+            switch (tag) {
+            case 22: {
+              normal = R3{-1, 0, 0};
+              break;
+            }
+            case 23: {
+              normal = R3{1, 0, 0};
+              break;
+            }
+            case 25: {
+              normal = R3{0, 0, -1};
+              break;
+            }
+            case 27: {
+              normal = R3{0, -1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(24);
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, numbered_boundary_descriptor),
+                                "error: invalid boundary \"ZMAX(24)\": boundary is not flat!");
+          }
+
+          {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(26);
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, numbered_boundary_descriptor),
+                                "error: invalid boundary \"YMAX(26)\": boundary is not flat!");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "ZMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshFlatNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 normal = zero;
+
+            if (name == "XMIN") {
+              normal = R3{-1, 0, 0};
+            } else if (name == "XMAX") {
+              normal = R3{1, 0, 0};
+            } else if (name == "YMIN") {
+              normal = R3{0, -1, 0};
+            } else if (name == "ZMIN") {
+              normal = R3{0, 0, -1};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.outgoingNormal() - normal) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NamedBoundaryDescriptor named_boundary_descriptor("YMAX");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, named_boundary_descriptor),
+                                "error: invalid boundary \"YMAX(26)\": boundary is not flat!");
+          }
+
+          {
+            NamedBoundaryDescriptor named_boundary_descriptor("ZMAX");
+            REQUIRE_THROWS_WITH(getMeshFlatNodeBoundary(mesh, named_boundary_descriptor),
+                                "error: invalid boundary \"ZMAX(24)\": boundary is not flat!");
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/tests/test_MeshLineEdgeBoundary.cpp b/tests/test_MeshLineEdgeBoundary.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b4b21d0dc7e692216f80d0119f97d423518eb884
--- /dev/null
+++ b/tests/test_MeshLineEdgeBoundary.cpp
@@ -0,0 +1,1258 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+
+#include <algebra/TinyMatrix.hpp>
+#include <mesh/Connectivity.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshLineEdgeBoundary.hpp>
+#include <mesh/NamedBoundaryDescriptor.hpp>
+#include <mesh/NumberedBoundaryDescriptor.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("MeshLineEdgeBoundary", "[mesh]")
+{
+  auto is_same = [](const auto& a, const auto& b) -> bool {
+    if (a.size() > 0 and b.size() > 0) {
+      return (a[0] == b[0]);
+    } else {
+      return (a.size() == b.size());
+    }
+  };
+
+  auto get_edge_list_from_tag = [](const size_t tag, const auto& connectivity) -> Array<const EdgeId> {
+    for (size_t i = 0; i < connectivity.template numberOfRefItemList<ItemType::edge>(); ++i) {
+      const auto& ref_edge_list = connectivity.template refItemList<ItemType::edge>(i);
+      const RefId ref_id        = ref_edge_list.refId();
+      if (ref_id.tagNumber() == tag) {
+        return ref_edge_list.list();
+      }
+    }
+    return {};
+  };
+
+  auto get_edge_list_from_name = [](const std::string& name, const auto& connectivity) -> Array<const EdgeId> {
+    for (size_t i = 0; i < connectivity.template numberOfRefItemList<ItemType::edge>(); ++i) {
+      const auto& ref_edge_list = connectivity.template refItemList<ItemType::edge>(i);
+      const RefId ref_id        = ref_edge_list.refId();
+      if (ref_id.tagName() == name) {
+        return ref_edge_list.list();
+      }
+    }
+    return {};
+  };
+
+  SECTION("aligned axis")
+  {
+    SECTION("2D")
+    {
+      static constexpr size_t Dimension = 2;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R2 = TinyVector<2>;
+
+      SECTION("cartesian 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian2DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {0, 1, 2, 3};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& edge_boundary = getMeshLineEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R2 direction = zero;
+
+            switch (tag) {
+            case 0: {
+              direction = R2{0, 1};
+              break;
+            }
+            case 1: {
+              direction = R2{0, 1};
+              break;
+            }
+            case 2: {
+              direction = R2{1, 0};
+              break;
+            }
+            case 3: {
+              direction = R2{1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(edge_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(10)),
+                                "error: cannot find edge list with name \"10\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(11)),
+                                "error: cannot find edge list with name \"11\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(12)),
+                                "error: cannot find edge list with name \"12\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(13)),
+                                "error: cannot find edge list with name \"13\"");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& edge_boundary = getMeshLineEdgeBoundary(mesh, named_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_name(name, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R2 direction = zero;
+
+            if (name == "XMIN") {
+              direction = R2{0, 1};
+            } else if (name == "XMAX") {
+              direction = R2{0, 1};
+            } else if (name == "YMIN") {
+              direction = R2{1, 0};
+            } else if (name == "YMAX") {
+              direction = R2{1, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(edge_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMIN")),
+                                "error: cannot find edge list with name \"XMINYMIN\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAX")),
+                                "error: cannot find edge list with name \"XMINYMAX\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMIN")),
+                                "error: cannot find edge list with name \"XMAXYMIN\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAX")),
+                                "error: cannot find edge list with name \"XMAXYMAX\"");
+          }
+        }
+      }
+
+      SECTION("hybrid 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {1, 2, 3, 4};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& edge_boundary = getMeshLineEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R2 direction = zero;
+
+            switch (tag) {
+            case 1: {
+              direction = R2{0, 1};
+              break;
+            }
+            case 2: {
+              direction = R2{0, 1};
+              break;
+            }
+            case 3: {
+              direction = R2{1, 0};
+              break;
+            }
+            case 4: {
+              direction = R2{1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(edge_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(8)),
+                                "error: cannot find edge list with name \"8\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(9)),
+                                "error: cannot find edge list with name \"9\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(10)),
+                                "error: cannot find edge list with name \"10\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(11)),
+                                "error: cannot find edge list with name \"11\"");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& edge_boundary = getMeshLineEdgeBoundary(mesh, named_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_name(name, connectivity);
+
+            R2 direction = zero;
+
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            if (name == "XMIN") {
+              direction = R2{0, 1};
+            } else if (name == "XMAX") {
+              direction = R2{0, 1};
+            } else if (name == "YMIN") {
+              direction = R2{1, 0};
+            } else if (name == "YMAX") {
+              direction = R2{1, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(edge_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMIN")),
+                                "error: cannot find edge list with name \"XMINYMIN\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAX")),
+                                "error: cannot find edge list with name \"XMINYMAX\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMIN")),
+                                "error: cannot find edge list with name \"XMAXYMIN\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAX")),
+                                "error: cannot find edge list with name \"XMAXYMAX\"");
+          }
+        }
+      }
+    }
+
+    SECTION("3D")
+    {
+      static constexpr size_t Dimension = 3;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R3 = TinyVector<3>;
+
+      SECTION("cartesian 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian3DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& edge_boundary = getMeshLineEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R3 direction = zero;
+
+            switch (tag) {
+            case 20:
+            case 21:
+            case 22:
+            case 23: {
+              direction = R3{0, 0, -1};
+              break;
+            }
+            case 24:
+            case 25:
+            case 26:
+            case 27: {
+              direction = R3{0, -1, 0};
+              break;
+            }
+            case 28:
+            case 29:
+            case 30:
+            case 31: {
+              direction = R3{1, 0, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(edge_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(0)),
+                              "error: invalid boundary \"XMIN(0)\": boundary is not a line!");
+          REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(1)),
+                              "error: invalid boundary \"XMAX(1)\": boundary is not a line!");
+          REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(2)),
+                              "error: invalid boundary \"YMIN(2)\": boundary is not a line!");
+          REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(3)),
+                              "error: invalid boundary \"YMAX(3)\": boundary is not a line!");
+          REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(4)),
+                              "error: invalid boundary \"ZMIN(4)\": boundary is not a line!");
+          REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(5)),
+                              "error: invalid boundary \"ZMAX(5)\": boundary is not a line!");
+
+          REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(10)),
+                              "error: cannot find edge list with name \"10\"");
+          REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(11)),
+                              "error: cannot find edge list with name \"11\"");
+          REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(12)),
+                              "error: cannot find edge list with name \"12\"");
+          REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(13)),
+                              "error: cannot find edge list with name \"13\"");
+          REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(14)),
+                              "error: cannot find edge list with name \"14\"");
+          REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(15)),
+                              "error: cannot find edge list with name \"15\"");
+          REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(16)),
+                              "error: cannot find edge list with name \"16\"");
+          REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(17)),
+                              "error: cannot find edge list with name \"17\"");
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMINYMIN", "XMINYMAX", "XMAXYMIN", "XMAXYMAX",
+                                                  "XMINZMIN", "XMINZMAX", "XMAXZMAX", "XMINZMAX",
+                                                  "YMINZMIN", "YMINZMAX", "YMAXZMAX", "YMAXZMIN"};
+
+          for (const auto& name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& edge_boundary = getMeshLineEdgeBoundary(mesh, named_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R3 direction = zero;
+
+            if ((name == "XMINYMIN") or (name == "XMINYMAX") or (name == "XMAXYMIN") or (name == "XMAXYMAX")) {
+              direction = R3{0, 0, -1};
+            } else if ((name == "XMINZMIN") or (name == "XMINZMAX") or (name == "XMAXZMIN") or (name == "XMAXZMAX")) {
+              direction = R3{0, -1, 0};
+            } else if ((name == "YMINZMIN") or (name == "YMINZMAX") or (name == "YMAXZMIN") or (name == "YMAXZMAX")) {
+              direction = R3{1, 0, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(edge_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMIN")),
+                                "error: invalid boundary \"XMIN(0)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAX")),
+                                "error: invalid boundary \"XMAX(1)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("YMIN")),
+                                "error: invalid boundary \"YMIN(2)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("YMAX")),
+                                "error: invalid boundary \"YMAX(3)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("ZMIN")),
+                                "error: invalid boundary \"ZMIN(4)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("ZMAX")),
+                                "error: invalid boundary \"ZMAX(5)\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMINZMIN")),
+                                "error: cannot find edge list with name \"XMINYMINZMIN\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMINZMIN")),
+                                "error: cannot find edge list with name \"XMAXYMINZMIN\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAXZMIN")),
+                                "error: cannot find edge list with name \"XMAXYMAXZMIN\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAXZMIN")),
+                                "error: cannot find edge list with name \"XMINYMAXZMIN\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMINZMAX")),
+                                "error: cannot find edge list with name \"XMINYMINZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMINZMAX")),
+                                "error: cannot find edge list with name \"XMAXYMINZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAXZMAX")),
+                                "error: cannot find edge list with name \"XMAXYMAXZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAXZMAX")),
+                                "error: cannot find edge list with name \"XMINYMAXZMAX\"");
+          }
+        }
+      }
+
+      SECTION("hybrid 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& edge_boundary = getMeshLineEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R3 direction = zero;
+
+            switch (tag) {
+            case 30:
+            case 31:
+            case 34:
+            case 35: {
+              direction = R3{0, 0, -1};
+              break;
+            }
+            case 28:
+            case 29:
+            case 32:
+            case 33: {
+              direction = R3{0, -1, 0};
+              break;
+            }
+            case 36:
+            case 37:
+            case 38:
+            case 39: {
+              direction = R3{1, 0, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(edge_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(22)),
+                                "error: invalid boundary \"XMIN(22)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(23)),
+                                "error: invalid boundary \"XMAX(23)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(24)),
+                                "error: invalid boundary \"ZMAX(24)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(25)),
+                                "error: invalid boundary \"ZMIN(25)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(26)),
+                                "error: invalid boundary \"YMAX(26)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(27)),
+                                "error: invalid boundary \"YMIN(27)\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(40)),
+                                "error: cannot find edge list with name \"40\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(41)),
+                                "error: cannot find edge list with name \"41\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(42)),
+                                "error: cannot find edge list with name \"42\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(43)),
+                                "error: cannot find edge list with name \"43\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(44)),
+                                "error: cannot find edge list with name \"44\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(45)),
+                                "error: cannot find edge list with name \"45\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(47)),
+                                "error: cannot find edge list with name \"47\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(51)),
+                                "error: cannot find edge list with name \"51\"");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMINYMIN", "XMINYMAX", "XMAXYMIN", "XMAXYMAX",
+                                                  "XMINZMIN", "XMINZMAX", "XMAXZMAX", "XMINZMAX",
+                                                  "YMINZMIN", "YMINZMAX", "YMAXZMAX", "YMAXZMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& edge_boundary = getMeshLineEdgeBoundary(mesh, named_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R3 direction = zero;
+
+            if ((name == "XMINYMIN") or (name == "XMINYMAX") or (name == "XMAXYMIN") or (name == "XMAXYMAX")) {
+              direction = R3{0, 0, -1};
+            } else if ((name == "XMINZMIN") or (name == "XMINZMAX") or (name == "XMAXZMIN") or (name == "XMAXZMAX")) {
+              direction = R3{0, -1, 0};
+            } else if ((name == "YMINZMIN") or (name == "YMINZMAX") or (name == "YMAXZMIN") or (name == "YMAXZMAX")) {
+              direction = R3{1, 0, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(edge_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMIN")),
+                                "error: invalid boundary \"XMIN(22)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAX")),
+                                "error: invalid boundary \"XMAX(23)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("YMIN")),
+                                "error: invalid boundary \"YMIN(27)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("YMAX")),
+                                "error: invalid boundary \"YMAX(26)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("ZMIN")),
+                                "error: invalid boundary \"ZMIN(25)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("ZMAX")),
+                                "error: invalid boundary \"ZMAX(24)\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMINZMIN")),
+                                "error: cannot find edge list with name \"XMINYMINZMIN\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMINZMIN")),
+                                "error: cannot find edge list with name \"XMAXYMINZMIN\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAXZMIN")),
+                                "error: cannot find edge list with name \"XMAXYMAXZMIN\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAXZMIN")),
+                                "error: cannot find edge list with name \"XMINYMAXZMIN\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMINZMAX")),
+                                "error: cannot find edge list with name \"XMINYMINZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMINZMAX")),
+                                "error: cannot find edge list with name \"XMAXYMINZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAXZMAX")),
+                                "error: cannot find edge list with name \"XMAXYMAXZMAX\"");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAXZMAX")),
+                                "error: cannot find edge list with name \"XMINYMAXZMAX\"");
+          }
+        }
+      }
+    }
+  }
+
+  SECTION("rotated axis")
+  {
+    SECTION("2D")
+    {
+      static constexpr size_t Dimension = 2;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R2 = TinyVector<2>;
+
+      const double theta = 0.3;
+      const TinyMatrix<2> R{std::cos(theta), -std::sin(theta),   //
+                            std::sin(theta), std::cos(theta)};
+
+      SECTION("cartesian 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian2DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R2> rotated_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {0, 1, 2, 3};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& edge_boundary = getMeshLineEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R2 direction = zero;
+
+            switch (tag) {
+            case 0: {
+              direction = R * R2{0, -1};
+              break;
+            }
+            case 1: {
+              direction = R * R2{0, -1};
+              break;
+            }
+            case 2: {
+              direction = R * R2{1, 0};
+              break;
+            }
+            case 3: {
+              direction = R * R2{1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(edge_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& edge_boundary = getMeshLineEdgeBoundary(mesh, named_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_name(name, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R2 direction = zero;
+
+            if (name == "XMIN") {
+              direction = R * R2{0, -1};
+            } else if (name == "XMAX") {
+              direction = R * R2{0, -1};
+            } else if (name == "YMIN") {
+              direction = R * R2{1, 0};
+            } else if (name == "YMAX") {
+              direction = R * R2{1, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(edge_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+
+      SECTION("hybrid 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R2> rotated_xr{connectivity};
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {1, 2, 3, 4};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& edge_boundary = getMeshLineEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R2 direction = zero;
+
+            switch (tag) {
+            case 1: {
+              direction = R * R2{0, -1};
+              break;
+            }
+            case 2: {
+              direction = R * R2{0, -1};
+              break;
+            }
+            case 3: {
+              direction = R * R2{1, 0};
+              break;
+            }
+            case 4: {
+              direction = R * R2{1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(edge_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& edge_boundary = getMeshLineEdgeBoundary(mesh, named_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_name(name, connectivity);
+
+            R2 direction = zero;
+
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            if (name == "XMIN") {
+              direction = R * R2{0, -1};
+            } else if (name == "XMAX") {
+              direction = R * R2{0, -1};
+            } else if (name == "YMIN") {
+              direction = R * R2{1, 0};
+            } else if (name == "YMAX") {
+              direction = R * R2{1, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(edge_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+    }
+
+    SECTION("3D")
+    {
+      static constexpr size_t Dimension = 3;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R3 = TinyVector<3>;
+
+      const double theta = 0.3;
+      const double phi   = 0.4;
+      const TinyMatrix<3> R =
+        TinyMatrix<3>{std::cos(theta), -std::sin(theta), 0, std::sin(theta), std::cos(theta), 0, 0, 0, 1} *
+        TinyMatrix<3>{0, std::cos(phi), -std::sin(phi), 0, std::sin(phi), std::cos(phi), 1, 0, 0};
+
+      SECTION("cartesian 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian3DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R3> rotated_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& edge_boundary = getMeshLineEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R3 direction = zero;
+
+            switch (tag) {
+            case 20:
+            case 21:
+            case 22:
+            case 23: {
+              direction = R * R3{0, 0, -1};
+              break;
+            }
+            case 24:
+            case 25:
+            case 26:
+            case 27: {
+              direction = R * R3{0, 1, 0};
+              break;
+            }
+            case 28:
+            case 29:
+            case 30:
+            case 31: {
+              direction = R * R3{-1, 0, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(edge_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMINYMIN", "XMINYMAX", "XMAXYMIN", "XMAXYMAX",
+                                                  "XMINZMIN", "XMINZMAX", "XMAXZMAX", "XMINZMAX",
+                                                  "YMINZMIN", "YMINZMAX", "YMAXZMAX", "YMAXZMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& edge_boundary = getMeshLineEdgeBoundary(mesh, named_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R3 direction = zero;
+
+            if ((name == "XMINYMIN") or (name == "XMINYMAX") or (name == "XMAXYMIN") or (name == "XMAXYMAX")) {
+              direction = R * R3{0, 0, -1};
+            } else if ((name == "XMINZMIN") or (name == "XMINZMAX") or (name == "XMAXZMIN") or (name == "XMAXZMAX")) {
+              direction = R * R3{0, 1, 0};
+            } else if ((name == "YMINZMIN") or (name == "YMINZMAX") or (name == "YMAXZMIN") or (name == "YMAXZMAX")) {
+              direction = R * R3{-1, 0, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(edge_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+
+      SECTION("hybrid 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R3> rotated_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& edge_boundary = getMeshLineEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R3 direction = zero;
+
+            switch (tag) {
+            case 30:
+            case 31:
+            case 34:
+            case 35: {
+              direction = R * R3{0, 0, -1};
+              break;
+            }
+            case 28:
+            case 29:
+            case 32:
+            case 33: {
+              direction = R * R3{0, 1, 0};
+              break;
+            }
+            case 36:
+            case 37:
+            case 38:
+            case 39: {
+              direction = R * R3{-1, 0, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(edge_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMINYMIN", "XMINYMAX", "XMAXYMIN", "XMAXYMAX",
+                                                  "XMINZMIN", "XMINZMAX", "XMAXZMAX", "XMINZMAX",
+                                                  "YMINZMIN", "YMINZMAX", "YMAXZMAX", "YMAXZMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& edge_boundary = getMeshLineEdgeBoundary(mesh, named_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R3 direction = zero;
+
+            if ((name == "XMINYMIN") or (name == "XMINYMAX") or (name == "XMAXYMIN") or (name == "XMAXYMAX")) {
+              direction = R * R3{0, 0, -1};
+            } else if ((name == "XMINZMIN") or (name == "XMINZMAX") or (name == "XMAXZMIN") or (name == "XMAXZMAX")) {
+              direction = R * R3{0, 1, 0};
+            } else if ((name == "YMINZMIN") or (name == "YMINZMAX") or (name == "YMAXZMIN") or (name == "YMAXZMAX")) {
+              direction = R * R3{-1, 0, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(edge_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+    }
+  }
+
+  SECTION("curved mesh")
+  {
+    SECTION("2D")
+    {
+      static constexpr size_t Dimension = 2;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R2 = TinyVector<2>;
+
+      auto curve = [](const R2& X) -> R2 { return R2{X[0], (1 + X[0] * X[0]) * X[1]}; };
+
+      SECTION("hybrid 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<TinyVector<2>> curved_xr{connectivity};
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { curved_xr[node_id] = curve(xr[node_id]); });
+
+        MeshType mesh{p_mesh->shared_connectivity(), curved_xr};
+
+        {
+          const std::set<size_t> tag_set = {1, 2, 4};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& edge_boundary = getMeshLineEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R2 direction = zero;
+
+            switch (tag) {
+            case 1: {
+              direction = R2{0, 1};
+              break;
+            }
+            case 2: {
+              direction = R2{0, 1};
+              break;
+            }
+            case 4: {
+              direction = R2{1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(edge_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(3);
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, numbered_boundary_descriptor),
+                                "error: invalid boundary \"YMAX(3)\": boundary is not a line!");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& edge_boundary = getMeshLineEdgeBoundary(mesh, named_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_name(name, connectivity);
+
+            R2 direction = zero;
+
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            if (name == "XMIN") {
+              direction = R2{0, 1};
+            } else if (name == "XMAX") {
+              direction = R2{0, 1};
+            } else if (name == "YMIN") {
+              direction = R2{1, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(edge_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NamedBoundaryDescriptor named_boundary_descriptor("YMAX");
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, named_boundary_descriptor),
+                                "error: invalid boundary \"YMAX(3)\": boundary is not a line!");
+          }
+        }
+      }
+    }
+
+    SECTION("3D")
+    {
+      static constexpr size_t Dimension = 3;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R3 = TinyVector<3>;
+
+      SECTION("cartesian 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian3DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        auto curve = [](const R3& X) -> R3 {
+          return R3{X[0], (1 + X[0] * X[0]) * (X[1] + 1), (1 - 0.2 * X[0] * X[0]) * X[2]};
+        };
+
+        NodeValue<R3> curved_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { curved_xr[node_id] = curve(xr[node_id]); });
+
+        MeshType mesh{p_mesh->shared_connectivity(), curved_xr};
+
+        {
+          const std::set<size_t> tag_set = {20, 21, 22, 23, 24, 25, 26, 27, 28};
+
+          for (auto tag : tag_set) {
+            auto edge_list = get_edge_list_from_tag(tag, connectivity);
+
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& edge_boundary = getMeshLineEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R3 direction = zero;
+
+            switch (tag) {
+            case 20:
+            case 21:
+            case 22:
+            case 23: {
+              direction = R3{0, 0, -1};
+              break;
+            }
+            case 24:
+            case 25:
+            case 26:
+            case 27: {
+              direction = R3{0, -1, 0};
+              break;
+            }
+            case 28: {
+              direction = R3{1, 0, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(edge_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(29)),
+                                "error: invalid boundary \"YMINZMAX(29)\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(30)),
+                                "error: invalid boundary \"YMAXZMAX(30)\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(31)),
+                                "error: invalid boundary \"YMAXZMIN(31)\": boundary is not a line!");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMINYMIN", "XMINYMAX", "XMAXYMIN", "XMAXYMAX", "XMINZMIN",
+                                                  "XMINZMAX", "XMAXZMAX", "XMINZMAX", "YMINZMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& edge_boundary = getMeshLineEdgeBoundary(mesh, named_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R3 direction = zero;
+
+            if ((name == "XMINYMIN") or (name == "XMINYMAX") or (name == "XMAXYMIN") or (name == "XMAXYMAX")) {
+              direction = R3{0, 0, -1};
+            } else if ((name == "XMINZMIN") or (name == "XMINZMAX") or (name == "XMAXZMIN") or (name == "XMAXZMAX")) {
+              direction = R3{0, -1, 0};
+            } else if ((name == "YMINZMIN")) {
+              direction = R3{1, 0, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(edge_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("YMAXZMAX")),
+                                "error: invalid boundary \"YMAXZMAX(30)\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("YMINZMAX")),
+                                "error: invalid boundary \"YMINZMAX(29)\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("YMAXZMIN")),
+                                "error: invalid boundary \"YMAXZMIN(31)\": boundary is not a line!");
+          }
+        }
+      }
+
+      SECTION("hybrid 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        auto curve = [](const R3& X) -> R3 {
+          return R3{X[0], (1 + X[0] * X[0]) * X[1], (1 - 0.2 * X[0] * X[0]) * X[2]};
+        };
+
+        NodeValue<R3> curved_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { curved_xr[node_id] = curve(xr[node_id]); });
+
+        MeshType mesh{p_mesh->shared_connectivity(), curved_xr};
+
+        {
+          const std::set<size_t> tag_set = {28, 29, 30, 31, 32, 33, 34, 35, 36};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& edge_boundary = getMeshLineEdgeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R3 direction = zero;
+
+            switch (tag) {
+            case 30:
+            case 31:
+            case 34:
+            case 35: {
+              direction = R3{0, 0, -1};
+              break;
+            }
+            case 28:
+            case 29:
+            case 32:
+            case 33: {
+              direction = R3{0, -1, 0};
+              break;
+            }
+            case 36:
+            // case 37:
+            // case 38:
+            case 39: {
+              direction = R3{1, 0, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(edge_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(37)),
+                                "error: invalid boundary \"YMINZMAX(37)\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(38)),
+                                "error: invalid boundary \"YMAXZMIN(38)\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NumberedBoundaryDescriptor(39)),
+                                "error: invalid boundary \"YMAXZMAX(39)\": boundary is not a line!");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMINYMIN", "XMINYMAX", "XMAXYMIN", "XMAXYMAX", "XMINZMIN",
+                                                  "XMINZMAX", "XMAXZMAX", "XMINZMAX", "YMINZMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& edge_boundary = getMeshLineEdgeBoundary(mesh, named_boundary_descriptor);
+
+            auto edge_list = get_edge_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(edge_boundary.edgeList(), edge_list));
+
+            R3 direction = zero;
+
+            if ((name == "XMINYMIN") or (name == "XMINYMAX") or (name == "XMAXYMIN") or (name == "XMAXYMAX")) {
+              direction = R3{0, 0, -1};
+            } else if ((name == "XMINZMIN") or (name == "XMINZMAX") or (name == "XMAXZMIN") or (name == "XMAXZMAX")) {
+              direction = R3{0, -1, 0};
+            } else if ((name == "YMINZMIN")) {
+              direction = R3{1, 0, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(edge_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("YMAXZMAX")),
+                                "error: invalid boundary \"YMAXZMAX(39)\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("YMINZMAX")),
+                                "error: invalid boundary \"YMINZMAX(37)\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineEdgeBoundary(mesh, NamedBoundaryDescriptor("YMAXZMIN")),
+                                "error: invalid boundary \"YMAXZMIN(38)\": boundary is not a line!");
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/tests/test_MeshLineFaceBoundary.cpp b/tests/test_MeshLineFaceBoundary.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f5cb9eb18714f668d63a0a4c2fd40a04b7cbeee9
--- /dev/null
+++ b/tests/test_MeshLineFaceBoundary.cpp
@@ -0,0 +1,536 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+
+#include <algebra/TinyMatrix.hpp>
+#include <mesh/Connectivity.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshLineFaceBoundary.hpp>
+#include <mesh/NamedBoundaryDescriptor.hpp>
+#include <mesh/NumberedBoundaryDescriptor.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("MeshLineFaceBoundary", "[mesh]")
+{
+  auto is_same = [](const auto& a, const auto& b) -> bool {
+    if (a.size() > 0 and b.size() > 0) {
+      return (a[0] == b[0]);
+    } else {
+      return (a.size() == b.size());
+    }
+  };
+
+  auto get_face_list_from_tag = [](const size_t tag, const auto& connectivity) -> Array<const FaceId> {
+    for (size_t i = 0; i < connectivity.template numberOfRefItemList<ItemType::face>(); ++i) {
+      const auto& ref_face_list = connectivity.template refItemList<ItemType::face>(i);
+      const RefId ref_id        = ref_face_list.refId();
+      if (ref_id.tagNumber() == tag) {
+        return ref_face_list.list();
+      }
+    }
+    return {};
+  };
+
+  auto get_face_list_from_name = [](const std::string& name, const auto& connectivity) -> Array<const FaceId> {
+    for (size_t i = 0; i < connectivity.template numberOfRefItemList<ItemType::face>(); ++i) {
+      const auto& ref_face_list = connectivity.template refItemList<ItemType::face>(i);
+      const RefId ref_id        = ref_face_list.refId();
+      if (ref_id.tagName() == name) {
+        return ref_face_list.list();
+      }
+    }
+    return {};
+  };
+
+  SECTION("aligned axis")
+  {
+    SECTION("2D")
+    {
+      static constexpr size_t Dimension = 2;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R2 = TinyVector<2>;
+
+      SECTION("cartesian 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian2DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {0, 1, 2, 3};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& face_boundary = getMeshLineFaceBoundary(mesh, numbered_boundary_descriptor);
+
+            auto face_list = get_face_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R2 direction = zero;
+
+            switch (tag) {
+            case 0: {
+              direction = R2{0, 1};
+              break;
+            }
+            case 1: {
+              direction = R2{0, 1};
+              break;
+            }
+            case 2: {
+              direction = R2{1, 0};
+              break;
+            }
+            case 3: {
+              direction = R2{1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(face_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineFaceBoundary(mesh, NumberedBoundaryDescriptor(10)),
+                                "error: cannot find face list with name \"10\"");
+            REQUIRE_THROWS_WITH(getMeshLineFaceBoundary(mesh, NumberedBoundaryDescriptor(11)),
+                                "error: cannot find face list with name \"11\"");
+            REQUIRE_THROWS_WITH(getMeshLineFaceBoundary(mesh, NumberedBoundaryDescriptor(12)),
+                                "error: cannot find face list with name \"12\"");
+            REQUIRE_THROWS_WITH(getMeshLineFaceBoundary(mesh, NumberedBoundaryDescriptor(13)),
+                                "error: cannot find face list with name \"13\"");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& face_boundary = getMeshLineFaceBoundary(mesh, named_boundary_descriptor);
+
+            auto face_list = get_face_list_from_name(name, connectivity);
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R2 direction = zero;
+
+            if (name == "XMIN") {
+              direction = R2{0, 1};
+            } else if (name == "XMAX") {
+              direction = R2{0, 1};
+            } else if (name == "YMIN") {
+              direction = R2{1, 0};
+            } else if (name == "YMAX") {
+              direction = R2{1, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(face_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineFaceBoundary(mesh, NamedBoundaryDescriptor("XMINYMIN")),
+                                "error: cannot find face list with name \"XMINYMIN\"");
+            REQUIRE_THROWS_WITH(getMeshLineFaceBoundary(mesh, NamedBoundaryDescriptor("XMINYMAX")),
+                                "error: cannot find face list with name \"XMINYMAX\"");
+            REQUIRE_THROWS_WITH(getMeshLineFaceBoundary(mesh, NamedBoundaryDescriptor("XMAXYMIN")),
+                                "error: cannot find face list with name \"XMAXYMIN\"");
+            REQUIRE_THROWS_WITH(getMeshLineFaceBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAX")),
+                                "error: cannot find face list with name \"XMAXYMAX\"");
+          }
+        }
+      }
+
+      SECTION("hybrid 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {1, 2, 3, 4};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& face_boundary = getMeshLineFaceBoundary(mesh, numbered_boundary_descriptor);
+
+            auto face_list = get_face_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R2 direction = zero;
+
+            switch (tag) {
+            case 1: {
+              direction = R2{0, 1};
+              break;
+            }
+            case 2: {
+              direction = R2{0, 1};
+              break;
+            }
+            case 3: {
+              direction = R2{1, 0};
+              break;
+            }
+            case 4: {
+              direction = R2{1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(face_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineFaceBoundary(mesh, NumberedBoundaryDescriptor(8)),
+                                "error: cannot find face list with name \"8\"");
+            REQUIRE_THROWS_WITH(getMeshLineFaceBoundary(mesh, NumberedBoundaryDescriptor(9)),
+                                "error: cannot find face list with name \"9\"");
+            REQUIRE_THROWS_WITH(getMeshLineFaceBoundary(mesh, NumberedBoundaryDescriptor(10)),
+                                "error: cannot find face list with name \"10\"");
+            REQUIRE_THROWS_WITH(getMeshLineFaceBoundary(mesh, NumberedBoundaryDescriptor(11)),
+                                "error: cannot find face list with name \"11\"");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& face_boundary = getMeshLineFaceBoundary(mesh, named_boundary_descriptor);
+
+            auto face_list = get_face_list_from_name(name, connectivity);
+
+            R2 direction = zero;
+
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            if (name == "XMIN") {
+              direction = R2{0, 1};
+            } else if (name == "XMAX") {
+              direction = R2{0, 1};
+            } else if (name == "YMIN") {
+              direction = R2{1, 0};
+            } else if (name == "YMAX") {
+              direction = R2{1, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(face_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineFaceBoundary(mesh, NamedBoundaryDescriptor("XMINYMIN")),
+                                "error: cannot find face list with name \"XMINYMIN\"");
+            REQUIRE_THROWS_WITH(getMeshLineFaceBoundary(mesh, NamedBoundaryDescriptor("XMINYMAX")),
+                                "error: cannot find face list with name \"XMINYMAX\"");
+            REQUIRE_THROWS_WITH(getMeshLineFaceBoundary(mesh, NamedBoundaryDescriptor("XMAXYMIN")),
+                                "error: cannot find face list with name \"XMAXYMIN\"");
+            REQUIRE_THROWS_WITH(getMeshLineFaceBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAX")),
+                                "error: cannot find face list with name \"XMAXYMAX\"");
+          }
+        }
+      }
+    }
+  }
+
+  SECTION("rotated axis")
+  {
+    SECTION("2D")
+    {
+      static constexpr size_t Dimension = 2;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R2 = TinyVector<2>;
+
+      const double theta = 0.3;
+      const TinyMatrix<2> R{std::cos(theta), -std::sin(theta),   //
+                            std::sin(theta), std::cos(theta)};
+
+      SECTION("cartesian 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian2DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R2> rotated_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {0, 1, 2, 3};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& face_boundary = getMeshLineFaceBoundary(mesh, numbered_boundary_descriptor);
+
+            auto face_list = get_face_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R2 direction = zero;
+
+            switch (tag) {
+            case 0: {
+              direction = R * R2{0, -1};
+              break;
+            }
+            case 1: {
+              direction = R * R2{0, -1};
+              break;
+            }
+            case 2: {
+              direction = R * R2{1, 0};
+              break;
+            }
+            case 3: {
+              direction = R * R2{1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(face_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& face_boundary = getMeshLineFaceBoundary(mesh, named_boundary_descriptor);
+
+            auto face_list = get_face_list_from_name(name, connectivity);
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R2 direction = zero;
+
+            if (name == "XMIN") {
+              direction = R * R2{0, -1};
+            } else if (name == "XMAX") {
+              direction = R * R2{0, -1};
+            } else if (name == "YMIN") {
+              direction = R * R2{1, 0};
+            } else if (name == "YMAX") {
+              direction = R * R2{1, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(face_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+
+      SECTION("hybrid 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R2> rotated_xr{connectivity};
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {1, 2, 3, 4};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& face_boundary = getMeshLineFaceBoundary(mesh, numbered_boundary_descriptor);
+
+            auto face_list = get_face_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R2 direction = zero;
+
+            switch (tag) {
+            case 1: {
+              direction = R * R2{0, -1};
+              break;
+            }
+            case 2: {
+              direction = R * R2{0, -1};
+              break;
+            }
+            case 3: {
+              direction = R * R2{1, 0};
+              break;
+            }
+            case 4: {
+              direction = R * R2{1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(face_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& face_boundary = getMeshLineFaceBoundary(mesh, named_boundary_descriptor);
+
+            auto face_list = get_face_list_from_name(name, connectivity);
+
+            R2 direction = zero;
+
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            if (name == "XMIN") {
+              direction = R * R2{0, -1};
+            } else if (name == "XMAX") {
+              direction = R * R2{0, -1};
+            } else if (name == "YMIN") {
+              direction = R * R2{1, 0};
+            } else if (name == "YMAX") {
+              direction = R * R2{1, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(face_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+    }
+  }
+
+  SECTION("curved mesh")
+  {
+    SECTION("2D")
+    {
+      static constexpr size_t Dimension = 2;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R2 = TinyVector<2>;
+
+      auto curve = [](const R2& X) -> R2 { return R2{X[0], (1 + X[0] * X[0]) * X[1]}; };
+
+      SECTION("hybrid 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<TinyVector<2>> curved_xr{connectivity};
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { curved_xr[node_id] = curve(xr[node_id]); });
+
+        MeshType mesh{p_mesh->shared_connectivity(), curved_xr};
+
+        {
+          const std::set<size_t> tag_set = {1, 2, 4};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& face_boundary = getMeshLineFaceBoundary(mesh, numbered_boundary_descriptor);
+
+            auto face_list = get_face_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            R2 direction = zero;
+
+            switch (tag) {
+            case 1: {
+              direction = R2{0, 1};
+              break;
+            }
+            case 2: {
+              direction = R2{0, 1};
+              break;
+            }
+            case 4: {
+              direction = R2{1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(face_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(3);
+            REQUIRE_THROWS_WITH(getMeshLineFaceBoundary(mesh, numbered_boundary_descriptor),
+                                "error: invalid boundary \"YMAX(3)\": boundary is not a line!");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& face_boundary = getMeshLineFaceBoundary(mesh, named_boundary_descriptor);
+
+            auto face_list = get_face_list_from_name(name, connectivity);
+
+            R2 direction = zero;
+
+            REQUIRE(is_same(face_boundary.faceList(), face_list));
+
+            if (name == "XMIN") {
+              direction = R2{0, 1};
+            } else if (name == "XMAX") {
+              direction = R2{0, 1};
+            } else if (name == "YMIN") {
+              direction = R2{1, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(face_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NamedBoundaryDescriptor named_boundary_descriptor("YMAX");
+            REQUIRE_THROWS_WITH(getMeshLineFaceBoundary(mesh, named_boundary_descriptor),
+                                "error: invalid boundary \"YMAX(3)\": boundary is not a line!");
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/tests/test_MeshLineNodeBoundary.cpp b/tests/test_MeshLineNodeBoundary.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..36c151e2d4ddf14e21ad5defed6774a06cf6394e
--- /dev/null
+++ b/tests/test_MeshLineNodeBoundary.cpp
@@ -0,0 +1,1258 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+
+#include <algebra/TinyMatrix.hpp>
+#include <mesh/Connectivity.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshLineNodeBoundary.hpp>
+#include <mesh/NamedBoundaryDescriptor.hpp>
+#include <mesh/NumberedBoundaryDescriptor.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("MeshLineNodeBoundary", "[mesh]")
+{
+  auto is_same = [](const auto& a, const auto& b) -> bool {
+    if (a.size() > 0 and b.size() > 0) {
+      return (a[0] == b[0]);
+    } else {
+      return (a.size() == b.size());
+    }
+  };
+
+  auto get_node_list_from_tag = [](const size_t tag, const auto& connectivity) -> Array<const NodeId> {
+    for (size_t i = 0; i < connectivity.template numberOfRefItemList<ItemType::node>(); ++i) {
+      const auto& ref_node_list = connectivity.template refItemList<ItemType::node>(i);
+      const RefId ref_id        = ref_node_list.refId();
+      if (ref_id.tagNumber() == tag) {
+        return ref_node_list.list();
+      }
+    }
+    return {};
+  };
+
+  auto get_node_list_from_name = [](const std::string& name, const auto& connectivity) -> Array<const NodeId> {
+    for (size_t i = 0; i < connectivity.template numberOfRefItemList<ItemType::node>(); ++i) {
+      const auto& ref_node_list = connectivity.template refItemList<ItemType::node>(i);
+      const RefId ref_id        = ref_node_list.refId();
+      if (ref_id.tagName() == name) {
+        return ref_node_list.list();
+      }
+    }
+    return {};
+  };
+
+  SECTION("aligned axis")
+  {
+    SECTION("2D")
+    {
+      static constexpr size_t Dimension = 2;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R2 = TinyVector<2>;
+
+      SECTION("cartesian 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian2DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {0, 1, 2, 3};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R2 direction = zero;
+
+            switch (tag) {
+            case 0: {
+              direction = R2{0, 1};
+              break;
+            }
+            case 1: {
+              direction = R2{0, 1};
+              break;
+            }
+            case 2: {
+              direction = R2{1, 0};
+              break;
+            }
+            case 3: {
+              direction = R2{1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(10)),
+                                "error: invalid boundary \"XMINYMIN(10)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(11)),
+                                "error: invalid boundary \"XMAXYMIN(11)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(12)),
+                                "error: invalid boundary \"XMAXYMAX(12)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(13)),
+                                "error: invalid boundary \"XMINYMAX(13)\": unable to compute direction");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R2 direction = zero;
+
+            if (name == "XMIN") {
+              direction = R2{0, 1};
+            } else if (name == "XMAX") {
+              direction = R2{0, 1};
+            } else if (name == "YMIN") {
+              direction = R2{1, 0};
+            } else if (name == "YMAX") {
+              direction = R2{1, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMIN")),
+                                "error: invalid boundary \"XMINYMIN(10)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAX")),
+                                "error: invalid boundary \"XMINYMAX(13)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMIN")),
+                                "error: invalid boundary \"XMAXYMIN(11)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAX")),
+                                "error: invalid boundary \"XMAXYMAX(12)\": unable to compute direction");
+          }
+        }
+      }
+
+      SECTION("hybrid 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {1, 2, 3, 4};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R2 direction = zero;
+
+            switch (tag) {
+            case 1: {
+              direction = R2{0, 1};
+              break;
+            }
+            case 2: {
+              direction = R2{0, 1};
+              break;
+            }
+            case 3: {
+              direction = R2{1, 0};
+              break;
+            }
+            case 4: {
+              direction = R2{1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(8)),
+                                "error: invalid boundary \"XMINYMIN(8)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(9)),
+                                "error: invalid boundary \"XMINYMAX(9)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(10)),
+                                "error: invalid boundary \"XMAXYMIN(10)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(11)),
+                                "error: invalid boundary \"XMAXYMAX(11)\": unable to compute direction");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            R2 direction = zero;
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            if (name == "XMIN") {
+              direction = R2{0, 1};
+            } else if (name == "XMAX") {
+              direction = R2{0, 1};
+            } else if (name == "YMIN") {
+              direction = R2{1, 0};
+            } else if (name == "YMAX") {
+              direction = R2{1, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMIN")),
+                                "error: invalid boundary \"XMINYMIN(8)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAX")),
+                                "error: invalid boundary \"XMINYMAX(9)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMIN")),
+                                "error: invalid boundary \"XMAXYMIN(10)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAX")),
+                                "error: invalid boundary \"XMAXYMAX(11)\": unable to compute direction");
+          }
+        }
+      }
+    }
+
+    SECTION("3D")
+    {
+      static constexpr size_t Dimension = 3;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R3 = TinyVector<3>;
+
+      SECTION("cartesian 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian3DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 direction = zero;
+
+            switch (tag) {
+            case 20:
+            case 21:
+            case 22:
+            case 23: {
+              direction = R3{0, 0, -1};
+              break;
+            }
+            case 24:
+            case 25:
+            case 26:
+            case 27: {
+              direction = R3{0, -1, 0};
+              break;
+            }
+            case 28:
+            case 29:
+            case 30:
+            case 31: {
+              direction = R3{1, 0, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(0)),
+                              "error: invalid boundary \"XMIN(0)\": boundary is not a line!");
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(1)),
+                              "error: invalid boundary \"XMAX(1)\": boundary is not a line!");
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(2)),
+                              "error: invalid boundary \"YMIN(2)\": boundary is not a line!");
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(3)),
+                              "error: invalid boundary \"YMAX(3)\": boundary is not a line!");
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(4)),
+                              "error: invalid boundary \"ZMIN(4)\": boundary is not a line!");
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(5)),
+                              "error: invalid boundary \"ZMAX(5)\": boundary is not a line!");
+
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(10)),
+                              "error: invalid boundary \"XMINYMINZMIN(10)\": unable to compute direction");
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(11)),
+                              "error: invalid boundary \"XMAXYMINZMIN(11)\": unable to compute direction");
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(12)),
+                              "error: invalid boundary \"XMAXYMAXZMIN(12)\": unable to compute direction");
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(13)),
+                              "error: invalid boundary \"XMINYMAXZMIN(13)\": unable to compute direction");
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(14)),
+                              "error: invalid boundary \"XMINYMINZMAX(14)\": unable to compute direction");
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(15)),
+                              "error: invalid boundary \"XMAXYMINZMAX(15)\": unable to compute direction");
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(16)),
+                              "error: invalid boundary \"XMAXYMAXZMAX(16)\": unable to compute direction");
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(17)),
+                              "error: invalid boundary \"XMINYMAXZMAX(17)\": unable to compute direction");
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMINYMIN", "XMINYMAX", "XMAXYMIN", "XMAXYMAX",
+                                                  "XMINZMIN", "XMINZMAX", "XMAXZMAX", "XMINZMAX",
+                                                  "YMINZMIN", "YMINZMAX", "YMAXZMAX", "YMAXZMIN"};
+
+          for (const auto& name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 direction = zero;
+
+            if ((name == "XMINYMIN") or (name == "XMINYMAX") or (name == "XMAXYMIN") or (name == "XMAXYMAX")) {
+              direction = R3{0, 0, -1};
+            } else if ((name == "XMINZMIN") or (name == "XMINZMAX") or (name == "XMAXZMIN") or (name == "XMAXZMAX")) {
+              direction = R3{0, -1, 0};
+            } else if ((name == "YMINZMIN") or (name == "YMINZMAX") or (name == "YMAXZMIN") or (name == "YMAXZMAX")) {
+              direction = R3{1, 0, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMIN")),
+                                "error: invalid boundary \"XMIN(0)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAX")),
+                                "error: invalid boundary \"XMAX(1)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("YMIN")),
+                                "error: invalid boundary \"YMIN(2)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("YMAX")),
+                                "error: invalid boundary \"YMAX(3)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("ZMIN")),
+                                "error: invalid boundary \"ZMIN(4)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("ZMAX")),
+                                "error: invalid boundary \"ZMAX(5)\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMINZMIN")),
+                                "error: invalid boundary \"XMINYMINZMIN(10)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMINZMIN")),
+                                "error: invalid boundary \"XMAXYMINZMIN(11)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAXZMIN")),
+                                "error: invalid boundary \"XMAXYMAXZMIN(12)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAXZMIN")),
+                                "error: invalid boundary \"XMINYMAXZMIN(13)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMINZMAX")),
+                                "error: invalid boundary \"XMINYMINZMAX(14)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMINZMAX")),
+                                "error: invalid boundary \"XMAXYMINZMAX(15)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAXZMAX")),
+                                "error: invalid boundary \"XMAXYMAXZMAX(16)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAXZMAX")),
+                                "error: invalid boundary \"XMINYMAXZMAX(17)\": unable to compute direction");
+          }
+        }
+      }
+
+      SECTION("hybrid 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 direction = zero;
+
+            switch (tag) {
+            case 30:
+            case 31:
+            case 34:
+            case 35: {
+              direction = R3{0, 0, -1};
+              break;
+            }
+            case 28:
+            case 29:
+            case 32:
+            case 33: {
+              direction = R3{0, -1, 0};
+              break;
+            }
+            case 36:
+            case 37:
+            case 38:
+            case 39: {
+              direction = R3{1, 0, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(22)),
+                                "error: invalid boundary \"XMIN(22)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(23)),
+                                "error: invalid boundary \"XMAX(23)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(24)),
+                                "error: invalid boundary \"ZMAX(24)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(25)),
+                                "error: invalid boundary \"ZMIN(25)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(26)),
+                                "error: invalid boundary \"YMAX(26)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(27)),
+                                "error: invalid boundary \"YMIN(27)\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(40)),
+                                "error: invalid boundary \"XMINYMINZMIN(40)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(41)),
+                                "error: invalid boundary \"XMAXYMINZMIN(41)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(42)),
+                                "error: invalid boundary \"XMINYMAXZMIN(42)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(43)),
+                                "error: invalid boundary \"XMINYMAXZMAX(43)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(44)),
+                                "error: invalid boundary \"XMINYMINZMAX(44)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(45)),
+                                "error: invalid boundary \"XMAXYMINZMAX(45)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(47)),
+                                "error: invalid boundary \"XMAXYMAXZMAX(47)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(51)),
+                                "error: invalid boundary \"XMAXYMAXZMIN(51)\": unable to compute direction");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMINYMIN", "XMINYMAX", "XMAXYMIN", "XMAXYMAX",
+                                                  "XMINZMIN", "XMINZMAX", "XMAXZMAX", "XMINZMAX",
+                                                  "YMINZMIN", "YMINZMAX", "YMAXZMAX", "YMAXZMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 direction = zero;
+
+            if ((name == "XMINYMIN") or (name == "XMINYMAX") or (name == "XMAXYMIN") or (name == "XMAXYMAX")) {
+              direction = R3{0, 0, -1};
+            } else if ((name == "XMINZMIN") or (name == "XMINZMAX") or (name == "XMAXZMIN") or (name == "XMAXZMAX")) {
+              direction = R3{0, -1, 0};
+            } else if ((name == "YMINZMIN") or (name == "YMINZMAX") or (name == "YMAXZMIN") or (name == "YMAXZMAX")) {
+              direction = R3{1, 0, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMIN")),
+                                "error: invalid boundary \"XMIN(22)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAX")),
+                                "error: invalid boundary \"XMAX(23)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("YMIN")),
+                                "error: invalid boundary \"YMIN(27)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("YMAX")),
+                                "error: invalid boundary \"YMAX(26)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("ZMIN")),
+                                "error: invalid boundary \"ZMIN(25)\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("ZMAX")),
+                                "error: invalid boundary \"ZMAX(24)\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMINZMIN")),
+                                "error: invalid boundary \"XMINYMINZMIN(40)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMINZMIN")),
+                                "error: invalid boundary \"XMAXYMINZMIN(41)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAXZMIN")),
+                                "error: invalid boundary \"XMAXYMAXZMIN(51)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAXZMIN")),
+                                "error: invalid boundary \"XMINYMAXZMIN(42)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMINZMAX")),
+                                "error: invalid boundary \"XMINYMINZMAX(44)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMINZMAX")),
+                                "error: invalid boundary \"XMAXYMINZMAX(45)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAXZMAX")),
+                                "error: invalid boundary \"XMAXYMAXZMAX(47)\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAXZMAX")),
+                                "error: invalid boundary \"XMINYMAXZMAX(43)\": unable to compute direction");
+          }
+        }
+      }
+    }
+  }
+
+  SECTION("rotated axis")
+  {
+    SECTION("2D")
+    {
+      static constexpr size_t Dimension = 2;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R2 = TinyVector<2>;
+
+      const double theta = 0.3;
+      const TinyMatrix<2> R{std::cos(theta), -std::sin(theta),   //
+                            std::sin(theta), std::cos(theta)};
+
+      SECTION("cartesian 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian2DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R2> rotated_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {0, 1, 2, 3};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R2 direction = zero;
+
+            switch (tag) {
+            case 0: {
+              direction = R * R2{0, -1};
+              break;
+            }
+            case 1: {
+              direction = R * R2{0, -1};
+              break;
+            }
+            case 2: {
+              direction = R * R2{1, 0};
+              break;
+            }
+            case 3: {
+              direction = R * R2{1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R2 direction = zero;
+
+            if (name == "XMIN") {
+              direction = R * R2{0, -1};
+            } else if (name == "XMAX") {
+              direction = R * R2{0, -1};
+            } else if (name == "YMIN") {
+              direction = R * R2{1, 0};
+            } else if (name == "YMAX") {
+              direction = R * R2{1, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+
+      SECTION("hybrid 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R2> rotated_xr{connectivity};
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {1, 2, 3, 4};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R2 direction = zero;
+
+            switch (tag) {
+            case 1: {
+              direction = R * R2{0, -1};
+              break;
+            }
+            case 2: {
+              direction = R * R2{0, -1};
+              break;
+            }
+            case 3: {
+              direction = R * R2{1, 0};
+              break;
+            }
+            case 4: {
+              direction = R * R2{1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            R2 direction = zero;
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            if (name == "XMIN") {
+              direction = R * R2{0, -1};
+            } else if (name == "XMAX") {
+              direction = R * R2{0, -1};
+            } else if (name == "YMIN") {
+              direction = R * R2{1, 0};
+            } else if (name == "YMAX") {
+              direction = R * R2{1, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+    }
+
+    SECTION("3D")
+    {
+      static constexpr size_t Dimension = 3;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R3 = TinyVector<3>;
+
+      const double theta = 0.3;
+      const double phi   = 0.4;
+      const TinyMatrix<3> R =
+        TinyMatrix<3>{std::cos(theta), -std::sin(theta), 0, std::sin(theta), std::cos(theta), 0, 0, 0, 1} *
+        TinyMatrix<3>{0, std::cos(phi), -std::sin(phi), 0, std::sin(phi), std::cos(phi), 1, 0, 0};
+
+      SECTION("cartesian 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian3DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R3> rotated_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 direction = zero;
+
+            switch (tag) {
+            case 20:
+            case 21:
+            case 22:
+            case 23: {
+              direction = R * R3{0, 0, -1};
+              break;
+            }
+            case 24:
+            case 25:
+            case 26:
+            case 27: {
+              direction = R * R3{0, 1, 0};
+              break;
+            }
+            case 28:
+            case 29:
+            case 30:
+            case 31: {
+              direction = R * R3{-1, 0, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMINYMIN", "XMINYMAX", "XMAXYMIN", "XMAXYMAX",
+                                                  "XMINZMIN", "XMINZMAX", "XMAXZMAX", "XMINZMAX",
+                                                  "YMINZMIN", "YMINZMAX", "YMAXZMAX", "YMAXZMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 direction = zero;
+
+            if ((name == "XMINYMIN") or (name == "XMINYMAX") or (name == "XMAXYMIN") or (name == "XMAXYMAX")) {
+              direction = R * R3{0, 0, -1};
+            } else if ((name == "XMINZMIN") or (name == "XMINZMAX") or (name == "XMAXZMIN") or (name == "XMAXZMAX")) {
+              direction = R * R3{0, 1, 0};
+            } else if ((name == "YMINZMIN") or (name == "YMINZMAX") or (name == "YMAXZMIN") or (name == "YMAXZMAX")) {
+              direction = R * R3{-1, 0, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+
+      SECTION("hybrid 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R3> rotated_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 direction = zero;
+
+            switch (tag) {
+            case 30:
+            case 31:
+            case 34:
+            case 35: {
+              direction = R * R3{0, 0, -1};
+              break;
+            }
+            case 28:
+            case 29:
+            case 32:
+            case 33: {
+              direction = R * R3{0, 1, 0};
+              break;
+            }
+            case 36:
+            case 37:
+            case 38:
+            case 39: {
+              direction = R * R3{-1, 0, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMINYMIN", "XMINYMAX", "XMAXYMIN", "XMAXYMAX",
+                                                  "XMINZMIN", "XMINZMAX", "XMAXZMAX", "XMINZMAX",
+                                                  "YMINZMIN", "YMINZMAX", "YMAXZMAX", "YMAXZMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 direction = zero;
+
+            if ((name == "XMINYMIN") or (name == "XMINYMAX") or (name == "XMAXYMIN") or (name == "XMAXYMAX")) {
+              direction = R * R3{0, 0, -1};
+            } else if ((name == "XMINZMIN") or (name == "XMINZMAX") or (name == "XMAXZMIN") or (name == "XMAXZMAX")) {
+              direction = R * R3{0, 1, 0};
+            } else if ((name == "YMINZMIN") or (name == "YMINZMAX") or (name == "YMAXZMIN") or (name == "YMAXZMAX")) {
+              direction = R * R3{-1, 0, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+    }
+  }
+
+  SECTION("curved mesh")
+  {
+    SECTION("2D")
+    {
+      static constexpr size_t Dimension = 2;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R2 = TinyVector<2>;
+
+      auto curve = [](const R2& X) -> R2 { return R2{X[0], (1 + X[0] * X[0]) * X[1]}; };
+
+      SECTION("hybrid 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<TinyVector<2>> curved_xr{connectivity};
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { curved_xr[node_id] = curve(xr[node_id]); });
+
+        MeshType mesh{p_mesh->shared_connectivity(), curved_xr};
+
+        {
+          const std::set<size_t> tag_set = {1, 2, 4};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R2 direction = zero;
+
+            switch (tag) {
+            case 1: {
+              direction = R2{0, 1};
+              break;
+            }
+            case 2: {
+              direction = R2{0, 1};
+              break;
+            }
+            case 4: {
+              direction = R2{1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(3);
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, numbered_boundary_descriptor),
+                                "error: invalid boundary \"YMAX(3)\": boundary is not a line!");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            R2 direction = zero;
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            if (name == "XMIN") {
+              direction = R2{0, 1};
+            } else if (name == "XMAX") {
+              direction = R2{0, 1};
+            } else if (name == "YMIN") {
+              direction = R2{1, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NamedBoundaryDescriptor named_boundary_descriptor("YMAX");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, named_boundary_descriptor),
+                                "error: invalid boundary \"YMAX(3)\": boundary is not a line!");
+          }
+        }
+      }
+    }
+
+    SECTION("3D")
+    {
+      static constexpr size_t Dimension = 3;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R3 = TinyVector<3>;
+
+      SECTION("cartesian 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian3DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        auto curve = [](const R3& X) -> R3 {
+          return R3{X[0], (1 + X[0] * X[0]) * (X[1] + 1), (1 - 0.2 * X[0] * X[0]) * X[2]};
+        };
+
+        NodeValue<R3> curved_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { curved_xr[node_id] = curve(xr[node_id]); });
+
+        MeshType mesh{p_mesh->shared_connectivity(), curved_xr};
+
+        {
+          const std::set<size_t> tag_set = {20, 21, 22, 23, 24, 25, 26, 27, 28};
+
+          for (auto tag : tag_set) {
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 direction = zero;
+
+            switch (tag) {
+            case 20:
+            case 21:
+            case 22:
+            case 23: {
+              direction = R3{0, 0, -1};
+              break;
+            }
+            case 24:
+            case 25:
+            case 26:
+            case 27: {
+              direction = R3{0, -1, 0};
+              break;
+            }
+            case 28: {
+              direction = R3{1, 0, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(29)),
+                                "error: invalid boundary \"YMINZMAX(29)\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(30)),
+                                "error: invalid boundary \"YMAXZMAX(30)\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(31)),
+                                "error: invalid boundary \"YMAXZMIN(31)\": boundary is not a line!");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMINYMIN", "XMINYMAX", "XMAXYMIN", "XMAXYMAX", "XMINZMIN",
+                                                  "XMINZMAX", "XMAXZMAX", "XMINZMAX", "YMINZMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 direction = zero;
+
+            if ((name == "XMINYMIN") or (name == "XMINYMAX") or (name == "XMAXYMIN") or (name == "XMAXYMAX")) {
+              direction = R3{0, 0, -1};
+            } else if ((name == "XMINZMIN") or (name == "XMINZMAX") or (name == "XMAXZMIN") or (name == "XMAXZMAX")) {
+              direction = R3{0, -1, 0};
+            } else if ((name == "YMINZMIN")) {
+              direction = R3{1, 0, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("YMAXZMAX")),
+                                "error: invalid boundary \"YMAXZMAX(30)\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("YMINZMAX")),
+                                "error: invalid boundary \"YMINZMAX(29)\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("YMAXZMIN")),
+                                "error: invalid boundary \"YMAXZMIN(31)\": boundary is not a line!");
+          }
+        }
+      }
+
+      SECTION("hybrid 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        auto curve = [](const R3& X) -> R3 {
+          return R3{X[0], (1 + X[0] * X[0]) * X[1], (1 - 0.2 * X[0] * X[0]) * X[2]};
+        };
+
+        NodeValue<R3> curved_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { curved_xr[node_id] = curve(xr[node_id]); });
+
+        MeshType mesh{p_mesh->shared_connectivity(), curved_xr};
+
+        {
+          const std::set<size_t> tag_set = {28, 29, 30, 31, 32, 33, 34, 35, 36};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 direction = zero;
+
+            switch (tag) {
+            case 30:
+            case 31:
+            case 34:
+            case 35: {
+              direction = R3{0, 0, -1};
+              break;
+            }
+            case 28:
+            case 29:
+            case 32:
+            case 33: {
+              direction = R3{0, -1, 0};
+              break;
+            }
+            case 36:
+            // case 37:
+            // case 38:
+            case 39: {
+              direction = R3{1, 0, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(37)),
+                                "error: invalid boundary \"YMINZMAX(37)\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(38)),
+                                "error: invalid boundary \"YMAXZMIN(38)\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(39)),
+                                "error: invalid boundary \"YMAXZMAX(39)\": boundary is not a line!");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMINYMIN", "XMINYMAX", "XMAXYMIN", "XMAXYMAX", "XMINZMIN",
+                                                  "XMINZMAX", "XMAXZMAX", "XMINZMAX", "YMINZMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 direction = zero;
+
+            if ((name == "XMINYMIN") or (name == "XMINYMAX") or (name == "XMAXYMIN") or (name == "XMAXYMAX")) {
+              direction = R3{0, 0, -1};
+            } else if ((name == "XMINZMIN") or (name == "XMINZMAX") or (name == "XMAXZMIN") or (name == "XMAXZMAX")) {
+              direction = R3{0, -1, 0};
+            } else if ((name == "YMINZMIN")) {
+              direction = R3{1, 0, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("YMAXZMAX")),
+                                "error: invalid boundary \"YMAXZMAX(39)\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("YMINZMAX")),
+                                "error: invalid boundary \"YMINZMAX(37)\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("YMAXZMIN")),
+                                "error: invalid boundary \"YMAXZMIN(38)\": boundary is not a line!");
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/tests/test_MeshNodeBoundary.cpp b/tests/test_MeshNodeBoundary.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..2639e6370f9681024f0dab87367468a1df2a5002
--- /dev/null
+++ b/tests/test_MeshNodeBoundary.cpp
@@ -0,0 +1,353 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+
+#include <mesh/Connectivity.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshNodeBoundary.hpp>
+#include <mesh/NamedBoundaryDescriptor.hpp>
+#include <mesh/NumberedBoundaryDescriptor.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("MeshNodeBoundary", "[mesh]")
+{
+  auto is_same = [](const auto& a, const auto& b) -> bool {
+    if (a.size() > 0 and b.size() > 0) {
+      return (a.size() == b.size()) and (&(a[0]) == &(b[0]));
+    } else {
+      return (a.size() == b.size());
+    }
+  };
+
+  auto get_node_list_from_tag = [](const size_t tag, const auto& connectivity) -> Array<const NodeId> {
+    for (size_t i = 0; i < connectivity.template numberOfRefItemList<ItemType::node>(); ++i) {
+      const auto& ref_node_list = connectivity.template refItemList<ItemType::node>(i);
+      const RefId ref_id        = ref_node_list.refId();
+      if (ref_id.tagNumber() == tag) {
+        return ref_node_list.list();
+      }
+    }
+    return {};
+  };
+
+  auto get_node_list_from_name = [](const std::string& name, const auto& connectivity) -> Array<const NodeId> {
+    for (size_t i = 0; i < connectivity.template numberOfRefItemList<ItemType::node>(); ++i) {
+      const auto& ref_node_list = connectivity.template refItemList<ItemType::node>(i);
+      const RefId ref_id        = ref_node_list.refId();
+      if (ref_id.tagName() == name) {
+        return ref_node_list.list();
+      }
+    }
+    return {};
+  };
+
+  SECTION("1D")
+  {
+    static constexpr size_t Dimension = 1;
+
+    using ConnectivityType = Connectivity<Dimension>;
+    using MeshType         = Mesh<ConnectivityType>;
+
+    SECTION("cartesian 1d")
+    {
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian1DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      const ConnectivityType& connectivity = mesh.connectivity();
+
+      {
+        const std::set<size_t> tag_set = {0, 1};
+
+        for (auto tag : tag_set) {
+          NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+          const auto& node_boundary = getMeshNodeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto node_list = get_node_list_from_tag(tag, connectivity);
+          REQUIRE(is_same(node_boundary.nodeList(), node_list));
+        }
+      }
+
+      {
+        const std::set<std::string> name_set = {"XMIN", "XMAX"};
+
+        for (auto name : name_set) {
+          NamedBoundaryDescriptor named_boundary_descriptor(name);
+          const auto& node_boundary = getMeshNodeBoundary(mesh, named_boundary_descriptor);
+
+          auto node_list = get_node_list_from_name(name, connectivity);
+
+          REQUIRE(is_same(node_boundary.nodeList(), node_list));
+        }
+      }
+    }
+
+    SECTION("unordered 1d")
+    {
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().unordered1DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      const ConnectivityType& connectivity = mesh.connectivity();
+
+      {
+        const std::set<size_t> tag_set = {1, 2};
+
+        for (auto tag : tag_set) {
+          NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+          const auto& node_boundary = getMeshNodeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto node_list = get_node_list_from_tag(tag, connectivity);
+          REQUIRE(is_same(node_boundary.nodeList(), node_list));
+        }
+      }
+
+      {
+        const std::set<std::string> name_set = {"XMIN", "XMAX"};
+
+        for (auto name : name_set) {
+          NamedBoundaryDescriptor named_boundary_descriptor(name);
+          const auto& node_boundary = getMeshNodeBoundary(mesh, named_boundary_descriptor);
+
+          auto node_list = get_node_list_from_name(name, connectivity);
+          REQUIRE(is_same(node_boundary.nodeList(), node_list));
+        }
+      }
+    }
+  }
+
+  SECTION("2D")
+  {
+    static constexpr size_t Dimension = 2;
+
+    using ConnectivityType = Connectivity<Dimension>;
+    using MeshType         = Mesh<ConnectivityType>;
+
+    SECTION("cartesian 2d")
+    {
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian2DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      const ConnectivityType& connectivity = mesh.connectivity();
+
+      {
+        const std::set<size_t> tag_set = {0, 1, 2, 3, 10, 11, 12, 13};
+
+        for (auto tag : tag_set) {
+          NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+          const auto& node_boundary = getMeshNodeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto node_list = get_node_list_from_tag(tag, connectivity);
+          REQUIRE(is_same(node_boundary.nodeList(), node_list));
+        }
+      }
+
+      {
+        const std::set<std::string> name_set = {"XMIN",     "XMAX",     "YMIN",     "YMAX",
+                                                "XMINYMIN", "XMINYMAX", "XMAXYMIN", "XMAXYMAX"};
+
+        for (auto name : name_set) {
+          NamedBoundaryDescriptor numbered_boundary_descriptor(name);
+          const auto& node_boundary = getMeshNodeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto node_list = get_node_list_from_name(name, connectivity);
+          REQUIRE(is_same(node_boundary.nodeList(), node_list));
+        }
+      }
+    }
+
+    SECTION("hybrid 2d")
+    {
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      const ConnectivityType& connectivity = mesh.connectivity();
+
+      {
+        const std::set<size_t> tag_set = {1, 2, 3, 4, 8, 9, 10, 11};
+
+        for (auto tag : tag_set) {
+          NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+          const auto& node_boundary = getMeshNodeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto node_list = get_node_list_from_tag(tag, connectivity);
+          REQUIRE(is_same(node_boundary.nodeList(), node_list));
+        }
+      }
+
+      {
+        const std::set<std::string> name_set = {"XMIN",     "YMIN",     "XMAX",     "YMIN",
+                                                "XMINYMIN", "XMINYMAX", "XMAXYMIN", "XMAXYMAX"};
+
+        for (auto name : name_set) {
+          NamedBoundaryDescriptor numbered_boundary_descriptor(name);
+          const auto& node_boundary = getMeshNodeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto node_list = get_node_list_from_name(name, connectivity);
+          REQUIRE(is_same(node_boundary.nodeList(), node_list));
+        }
+      }
+    }
+  }
+
+  SECTION("3D")
+  {
+    static constexpr size_t Dimension = 3;
+
+    using ConnectivityType = Connectivity<Dimension>;
+    using MeshType         = Mesh<ConnectivityType>;
+
+    SECTION("cartesian 3d")
+    {
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian3DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      const ConnectivityType& connectivity = mesh.connectivity();
+
+      {
+        const std::set<size_t> tag_set = {0,  1,  2,  3,  4,  5,  10, 11, 12, 13, 14, 15, 16,
+                                          17, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31};
+
+        for (auto tag : tag_set) {
+          NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+          const auto& node_boundary = getMeshNodeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto node_list = get_node_list_from_tag(tag, connectivity);
+          REQUIRE(is_same(node_boundary.nodeList(), node_list));
+        }
+      }
+
+      {
+        const std::set<std::string> name_set = {// faces
+                                                "XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX",
+                                                // ridges
+                                                "XMINYMIN", "XMINYMAX", "XMINZMIN", "XMINZMAX", "XMAXYMIN", "XMAXYMAX",
+                                                "XMAXZMIN", "XMAXZMAX", "YMINZMIN", "YMINZMAX", "YMAXZMIN", "YMAXZMAX",
+                                                // corners
+                                                "XMINYMINZMIN", "XMINYMINZMAX", "XMINYMAXZMIN", "XMINYMAXZMAX",
+                                                "XMAXYMINZMIN", "XMAXYMINZMAX", "XMAXYMAXZMIN", "XMAXYMAXZMAX"};
+
+        for (auto name : name_set) {
+          NamedBoundaryDescriptor numbered_boundary_descriptor(name);
+          const auto& node_boundary = getMeshNodeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto node_list = get_node_list_from_name(name, connectivity);
+          REQUIRE(is_same(node_boundary.nodeList(), node_list));
+        }
+      }
+    }
+
+    SECTION("hybrid 3d")
+    {
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      const ConnectivityType& connectivity = mesh.connectivity();
+
+      {
+        const std::set<size_t> tag_set = {22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34,
+                                          35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 47, 51};
+
+        for (auto tag : tag_set) {
+          NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+          const auto& node_boundary = getMeshNodeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto node_list = get_node_list_from_tag(tag, connectivity);
+          REQUIRE(is_same(node_boundary.nodeList(), node_list));
+        }
+      }
+
+      {
+        const std::set<std::string> name_set = {// faces
+                                                "XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX",
+                                                // ridges
+                                                "XMINYMIN", "XMINYMAX", "XMINZMIN", "XMINZMAX", "XMAXYMIN", "XMAXYMAX",
+                                                "XMAXZMIN", "XMAXZMAX", "YMINZMIN", "YMINZMAX", "YMAXZMIN", "YMAXZMAX",
+                                                // corners
+                                                "XMINYMINZMIN", "XMINYMINZMAX", "XMINYMAXZMIN", "XMINYMAXZMAX",
+                                                "XMAXYMINZMIN", "XMAXYMINZMAX", "XMAXYMAXZMIN", "XMAXYMAXZMAX"};
+
+        for (auto name : name_set) {
+          NamedBoundaryDescriptor numbered_boundary_descriptor(name);
+          const auto& node_boundary = getMeshNodeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto node_list = get_node_list_from_name(name, connectivity);
+          REQUIRE(is_same(node_boundary.nodeList(), node_list));
+        }
+      }
+    }
+  }
+
+  SECTION("errors")
+  {
+    SECTION("cannot find boundary")
+    {
+      static constexpr size_t Dimension = 3;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      NamedBoundaryDescriptor named_boundary_descriptor("invalid_boundary");
+
+      REQUIRE_THROWS_WITH(getMeshNodeBoundary(mesh, named_boundary_descriptor),
+                          "error: cannot find node list with name \"invalid_boundary\"");
+    }
+
+    SECTION("surface is inside")
+    {
+      SECTION("1D")
+      {
+        static constexpr size_t Dimension = 1;
+
+        using ConnectivityType = Connectivity<Dimension>;
+        using MeshType         = Mesh<ConnectivityType>;
+
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().unordered1DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        NamedBoundaryDescriptor named_boundary_descriptor("INTERFACE");
+
+        REQUIRE_THROWS_WITH(getMeshNodeBoundary(mesh, named_boundary_descriptor),
+                            "error: invalid boundary \"INTERFACE(3)\": inner nodes cannot be used to define mesh "
+                            "boundaries");
+      }
+
+      SECTION("2D")
+      {
+        static constexpr size_t Dimension = 2;
+
+        using ConnectivityType = Connectivity<Dimension>;
+        using MeshType         = Mesh<ConnectivityType>;
+
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        NamedBoundaryDescriptor named_boundary_descriptor("INTERFACE");
+
+        REQUIRE_THROWS_WITH(getMeshNodeBoundary(mesh, named_boundary_descriptor),
+                            "error: invalid boundary \"INTERFACE(5)\": inner edges cannot be used to define mesh "
+                            "boundaries");
+      }
+
+      SECTION("3D")
+      {
+        static constexpr size_t Dimension = 3;
+
+        using ConnectivityType = Connectivity<Dimension>;
+        using MeshType         = Mesh<ConnectivityType>;
+
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        NamedBoundaryDescriptor named_boundary_descriptor("INTERFACE1");
+
+        REQUIRE_THROWS_WITH(getMeshNodeBoundary(mesh, named_boundary_descriptor),
+                            "error: invalid boundary \"INTERFACE1(55)\": inner faces cannot be used to define mesh "
+                            "boundaries");
+      }
+    }
+  }
+}
diff --git a/tests/test_RefItemList.cpp b/tests/test_RefItemList.cpp
index 4a9a7b7b2dc3a7aad46fd29703769d4550fc67dd..147d6543579bfeafb2ff12904dcc1935c7fbd451 100644
--- a/tests/test_RefItemList.cpp
+++ b/tests/test_RefItemList.cpp
@@ -12,16 +12,18 @@ TEST_CASE("RefItemList", "[mesh]")
     const Array<NodeId> node_id_array = convert_to_array(std::vector<NodeId>{1, 3, 7, 2, 4, 11});
     const RefId ref_id{3, "my_reference"};
 
-    RefItemList<ItemType::node> ref_node_list{ref_id, node_id_array};
+    RefItemList<ItemType::node> ref_node_list{ref_id, node_id_array, true};
     REQUIRE(ref_node_list.refId() == ref_id);
     REQUIRE(ref_node_list.list().size() == node_id_array.size());
     REQUIRE(&(ref_node_list.list()[0]) == &(node_id_array[0]));
+    REQUIRE(ref_node_list.isBoundary() == true);
 
     {
       RefItemList copy_ref_node_list{ref_node_list};
       REQUIRE(copy_ref_node_list.refId() == ref_id);
       REQUIRE(copy_ref_node_list.list().size() == node_id_array.size());
       REQUIRE(&(copy_ref_node_list.list()[0]) == &(node_id_array[0]));
+      REQUIRE(copy_ref_node_list.isBoundary() == true);
     }
 
     {
@@ -30,12 +32,14 @@ TEST_CASE("RefItemList", "[mesh]")
       REQUIRE(affect_ref_node_list.refId() == ref_id);
       REQUIRE(affect_ref_node_list.list().size() == node_id_array.size());
       REQUIRE(&(affect_ref_node_list.list()[0]) == &(node_id_array[0]));
+      REQUIRE(affect_ref_node_list.isBoundary() == true);
 
       RefItemList<ItemType::node> move_ref_node_list;
       move_ref_node_list = std::move(affect_ref_node_list);
       REQUIRE(move_ref_node_list.refId() == ref_id);
       REQUIRE(move_ref_node_list.list().size() == node_id_array.size());
       REQUIRE(&(move_ref_node_list.list()[0]) == &(node_id_array[0]));
+      REQUIRE(move_ref_node_list.isBoundary() == true);
     }
   }
 
@@ -44,16 +48,18 @@ TEST_CASE("RefItemList", "[mesh]")
     const Array<EdgeId> edge_id_array = convert_to_array(std::vector<EdgeId>{1, 3, 7, 2, 4, 11});
     const RefId ref_id{3, "my_reference"};
 
-    RefItemList<ItemType::edge> ref_edge_list{ref_id, edge_id_array};
+    RefItemList<ItemType::edge> ref_edge_list{ref_id, edge_id_array, false};
     REQUIRE(ref_edge_list.refId() == ref_id);
     REQUIRE(ref_edge_list.list().size() == edge_id_array.size());
     REQUIRE(&(ref_edge_list.list()[0]) == &(edge_id_array[0]));
+    REQUIRE(ref_edge_list.isBoundary() == false);
 
     {
       RefItemList copy_ref_edge_list{ref_edge_list};
       REQUIRE(copy_ref_edge_list.refId() == ref_id);
       REQUIRE(copy_ref_edge_list.list().size() == edge_id_array.size());
       REQUIRE(&(copy_ref_edge_list.list()[0]) == &(edge_id_array[0]));
+      REQUIRE(copy_ref_edge_list.isBoundary() == false);
     }
 
     {
@@ -62,12 +68,14 @@ TEST_CASE("RefItemList", "[mesh]")
       REQUIRE(affect_ref_edge_list.refId() == ref_id);
       REQUIRE(affect_ref_edge_list.list().size() == edge_id_array.size());
       REQUIRE(&(affect_ref_edge_list.list()[0]) == &(edge_id_array[0]));
+      REQUIRE(affect_ref_edge_list.isBoundary() == false);
 
       RefItemList<ItemType::edge> move_ref_edge_list;
       move_ref_edge_list = std::move(affect_ref_edge_list);
       REQUIRE(move_ref_edge_list.refId() == ref_id);
       REQUIRE(move_ref_edge_list.list().size() == edge_id_array.size());
       REQUIRE(&(move_ref_edge_list.list()[0]) == &(edge_id_array[0]));
+      REQUIRE(move_ref_edge_list.isBoundary() == false);
     }
   }
 
@@ -76,16 +84,18 @@ TEST_CASE("RefItemList", "[mesh]")
     const Array<FaceId> face_id_array = convert_to_array(std::vector<FaceId>{1, 3, 7, 2, 4, 11});
     const RefId ref_id{3, "my_reference"};
 
-    RefItemList<ItemType::face> ref_face_list{ref_id, face_id_array};
+    RefItemList<ItemType::face> ref_face_list{ref_id, face_id_array, true};
     REQUIRE(ref_face_list.refId() == ref_id);
     REQUIRE(ref_face_list.list().size() == face_id_array.size());
     REQUIRE(&(ref_face_list.list()[0]) == &(face_id_array[0]));
+    REQUIRE(ref_face_list.isBoundary() == true);
 
     {
       RefItemList copy_ref_face_list{ref_face_list};
       REQUIRE(copy_ref_face_list.refId() == ref_id);
       REQUIRE(copy_ref_face_list.list().size() == face_id_array.size());
       REQUIRE(&(copy_ref_face_list.list()[0]) == &(face_id_array[0]));
+      REQUIRE(copy_ref_face_list.isBoundary() == true);
     }
 
     {
@@ -94,12 +104,14 @@ TEST_CASE("RefItemList", "[mesh]")
       REQUIRE(affect_ref_face_list.refId() == ref_id);
       REQUIRE(affect_ref_face_list.list().size() == face_id_array.size());
       REQUIRE(&(affect_ref_face_list.list()[0]) == &(face_id_array[0]));
+      REQUIRE(affect_ref_face_list.isBoundary() == true);
 
       RefItemList<ItemType::face> move_ref_face_list;
       move_ref_face_list = std::move(affect_ref_face_list);
       REQUIRE(move_ref_face_list.refId() == ref_id);
       REQUIRE(move_ref_face_list.list().size() == face_id_array.size());
       REQUIRE(&(move_ref_face_list.list()[0]) == &(face_id_array[0]));
+      REQUIRE(move_ref_face_list.isBoundary() == true);
     }
   }
 
@@ -108,16 +120,18 @@ TEST_CASE("RefItemList", "[mesh]")
     const Array<CellId> cell_id_array = convert_to_array(std::vector<CellId>{1, 3, 7, 2, 4, 11});
     const RefId ref_id{3, "my_reference"};
 
-    RefItemList<ItemType::cell> ref_cell_list{ref_id, cell_id_array};
+    RefItemList<ItemType::cell> ref_cell_list{ref_id, cell_id_array, false};
     REQUIRE(ref_cell_list.refId() == ref_id);
     REQUIRE(ref_cell_list.list().size() == cell_id_array.size());
     REQUIRE(&(ref_cell_list.list()[0]) == &(cell_id_array[0]));
+    REQUIRE(ref_cell_list.isBoundary() == false);
 
     {
       RefItemList copy_ref_cell_list{ref_cell_list};
       REQUIRE(copy_ref_cell_list.refId() == ref_id);
       REQUIRE(copy_ref_cell_list.list().size() == cell_id_array.size());
       REQUIRE(&(copy_ref_cell_list.list()[0]) == &(cell_id_array[0]));
+      REQUIRE(copy_ref_cell_list.isBoundary() == false);
     }
 
     {
@@ -126,12 +140,14 @@ TEST_CASE("RefItemList", "[mesh]")
       REQUIRE(affect_ref_cell_list.refId() == ref_id);
       REQUIRE(affect_ref_cell_list.list().size() == cell_id_array.size());
       REQUIRE(&(affect_ref_cell_list.list()[0]) == &(cell_id_array[0]));
+      REQUIRE(affect_ref_cell_list.isBoundary() == false);
 
       RefItemList<ItemType::cell> move_ref_cell_list;
       move_ref_cell_list = std::move(affect_ref_cell_list);
       REQUIRE(move_ref_cell_list.refId() == ref_id);
       REQUIRE(move_ref_cell_list.list().size() == cell_id_array.size());
       REQUIRE(&(move_ref_cell_list.list()[0]) == &(cell_id_array[0]));
+      REQUIRE(move_ref_cell_list.isBoundary() == false);
     }
   }
 }