Repository: FEniCS/dolfinx
Pull request #3686: Extend VTKHDF file format to read/write higher order grids
Author: login: jorgensd
Labels: name: enhancement
description: New feature or request
name: io
Created: 2025-04-04T13:54:34Z | Merged: 2025-04-11T13:12:57Z

- VTKHDF now supports reading and writing higher order grids (arbitrary order).
    - CoordinateElement for degree > 2 will be equispaced.
    - The support includes mixed topology.
         - Test using a mesh with second order quadrilateral and second order triangular cells have been added.
- Added special handling of 1D and 2D geometries (`gdim` argument in read constructor).
    - This is because VTKHDF doesn't have a notion of 2D geometries, in the same way XDMF supported `XY` points and `XYZ` points.

Partially addresses #3582 


Diff:
diff --git a/cpp/dolfinx/io/VTKHDF.h b/cpp/dolfinx/io/VTKHDF.h
index 197a3ce98fc..5dec51a8d98 100644
--- a/cpp/dolfinx/io/VTKHDF.h
+++ b/cpp/dolfinx/io/VTKHDF.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Chris Richardson
+// Copyright (C) 2024-2025 Chris Richardson, Jørgen S. Dokken
 //
 // This file is part of DOLFINx (https://www.fenicsproject.org)
 //
@@ -29,9 +29,11 @@ void write_mesh(std::string filename, const mesh::Mesh<U>& mesh);
 /// @tparam U Scalar type of mesh
 /// @param comm MPI Communicator for reading mesh
 /// @param filename Filename
+/// @param gdim Geometric dimension
 /// @return a mesh
 template <typename U>
-mesh::Mesh<U> read_mesh(MPI_Comm comm, std::string filename);
+mesh::Mesh<U> read_mesh(MPI_Comm comm, std::string filename,
+                        std::size_t gdim = 3);
 
 } // namespace dolfinx::io::VTKHDF
 
@@ -85,9 +87,8 @@ void io::VTKHDF::write_mesh(std::string filename, const mesh::Mesh<U>& mesh)
 
   // Geometry dofmap and points
   auto geom_imap = mesh.geometry().index_map();
-  std::int32_t gdim = mesh.geometry().dim();
   std::int64_t size_global = geom_imap->size_global();
-  std::vector<std::int64_t> geom_global_shape = {size_global, gdim};
+  std::vector<std::int64_t> geom_global_shape = {size_global, 3};
   auto geom_irange = geom_imap->local_range();
 
   io::hdf5::write_dataset(h5file, "/VTKHDF/Points", mesh.geometry().x().data(),
@@ -108,9 +109,10 @@ void io::VTKHDF::write_mesh(std::string filename, const mesh::Mesh<U>& mesh)
     std::vector<std::int32_t> local_dm;
     local_dm.reserve(g_dofmap.extent(1) * num_cells[i]);
     auto perm = io::cells::perm_vtk(cell_types[i], g_dofmap.extent(1));
+    auto inverse_perm = io::cells::transpose(perm);
     for (int j = 0; j < num_cells[i]; ++j)
       for (int k = 0; k < g_dofmap.extent(1); ++k)
-        local_dm.push_back(g_dofmap(j, perm[k]));
+        local_dm.push_back(g_dofmap(j, inverse_perm[k]));
 
     std::vector<std::int64_t> global_dm(local_dm.size());
     geom_imap->local_to_global(local_dm, global_dm);
@@ -185,7 +187,8 @@ void io::VTKHDF::write_mesh(std::string filename, const mesh::Mesh<U>& mesh)
 }
 
 template <typename U>
-mesh::Mesh<U> io::VTKHDF::read_mesh(MPI_Comm comm, std::string filename)
+mesh::Mesh<U> io::VTKHDF ::read_mesh(MPI_Comm comm, std::string filename,
+                                     std::size_t gdim)
 {
   hid_t h5file = io::hdf5::open_file(comm, filename, "r", true);
 
@@ -199,7 +202,40 @@ mesh::Mesh<U> io::VTKHDF::read_mesh(MPI_Comm comm, std::string filename)
   std::vector<std::uint8_t> types
       = io::hdf5::read_dataset<std::uint8_t>(dset_id, local_cell_range, true);
   H5Dclose(dset_id);
-  std::vector<std::uint8_t> types_unique(types.begin(), types.end());
+  std::vector<std::array<std::uint8_t, 2>> types_unique;
+  types_unique.reserve(types.size());
+
+  // Create reverse map from VTK to dolfinx cell types
+  std::map<std::uint8_t, mesh::CellType> vtk_to_dolfinx;
+  constexpr std::array<mesh::CellType, 8> dolfinx_cells
+      = {mesh::CellType::point,       mesh::CellType::interval,
+         mesh::CellType::triangle,    mesh::CellType::quadrilateral,
+         mesh::CellType::tetrahedron, mesh::CellType::prism,
+         mesh::CellType::pyramid,     mesh::CellType::hexahedron};
+  for (auto dolfinx_type : dolfinx_cells)
+  {
+    vtk_to_dolfinx.insert({io::cells::get_vtk_cell_type(
+                               dolfinx_type, mesh::cell_dim(dolfinx_type)),
+                           dolfinx_type});
+  }
+
+  // Read in offsets to determine the different cell-types in the mesh
+  dset_id = io::hdf5::open_dataset(h5file, "/VTKHDF/Offsets");
+  std::vector<std::int64_t> offsets = io::hdf5::read_dataset<std::int64_t>(
+      dset_id, {local_cell_range[0], local_cell_range[1] + 1}, true);
+  H5Dclose(dset_id);
+
+  // Convert cell offsets to cell type and cell degree tuples
+  std::vector<std::uint8_t> cell_degrees;
+  cell_degrees.reserve(types.size());
+  for (std::size_t i = 0; i < types.size(); ++i)
+  {
+    std::uint8_t num_nodes = offsets[i + 1] - offsets[i];
+    std::uint8_t cell_degree
+        = io::cells::cell_degree(vtk_to_dolfinx.at(types[i]), num_nodes);
+    types_unique.push_back({types[i], cell_degree});
+    cell_degrees.push_back(cell_degree);
+  }
   {
     std::ranges::sort(types_unique);
     auto [unique_end, range_end] = std::ranges::unique(types_unique);
@@ -208,44 +244,61 @@ mesh::Mesh<U> io::VTKHDF::read_mesh(MPI_Comm comm, std::string filename)
 
   // Share cell types with all processes to make global list of cell types
   // FIXME: amount of data is small, but number of connections does not scale.
-  std::int32_t count = types_unique.size();
+  std::int32_t count = 2 * types_unique.size();
   std::vector<std::int32_t> recv_count(size);
   std::vector<std::int32_t> recv_offsets(size + 1, 0);
   MPI_Allgather(&count, 1, MPI_INT32_T, recv_count.data(), 1, MPI_INT32_T,
                 comm);
   std::partial_sum(recv_count.begin(), recv_count.end(),
                    recv_offsets.begin() + 1);
-  std::vector<std::uint8_t> recv_types(recv_offsets.back());
-  MPI_Allgatherv(types_unique.data(), count, MPI_UINT8_T, recv_types.data(),
-                 recv_count.data(), recv_offsets.data(), MPI_UINT8_T, comm);
-  {
-    std::ranges::sort(recv_types);
-    auto [unique_end, range_end] = std::ranges::unique(recv_types);
-    recv_types.erase(unique_end, range_end);
-  }
 
-  // Create reverse map from VTK to dolfinx cell types
-  std::map<std::uint8_t, mesh::CellType> vtk_to_dolfinx;
-  const std::vector<mesh::CellType> dolfinx_cells
-      = {mesh::CellType::point,       mesh::CellType::interval,
-         mesh::CellType::triangle,    mesh::CellType::quadrilateral,
-         mesh::CellType::tetrahedron, mesh::CellType::prism,
-         mesh::CellType::pyramid,     mesh::CellType::hexahedron};
-  for (auto dolfinx_type : dolfinx_cells)
+  std::vector<std::array<std::uint8_t, 2>> recv_types;
+  recv_types.reserve(recv_offsets.back());
   {
-    vtk_to_dolfinx.insert({io::cells::get_vtk_cell_type(
-                               dolfinx_type, mesh::cell_dim(dolfinx_type)),
-                           dolfinx_type});
+    std::vector<std::uint8_t> send_types;
+    send_types.reserve(count);
+    std::for_each(types_unique.begin(), types_unique.end(),
+                  [&send_types](const auto& t)
+                  {
+                    send_types.push_back(t.front());
+                    send_types.push_back(t.back());
+                  });
+
+    std::vector<std::uint8_t> recv_types_buffer(recv_offsets.back());
+    MPI_Allgatherv(send_types.data(), send_types.size(), MPI_UINT8_T,
+                   recv_types_buffer.data(), recv_count.data(),
+                   recv_offsets.data(), MPI_UINT8_T, comm);
+    md::mdspan<const std::uint8_t,
+               md::extents<std::size_t, md::dynamic_extent, 2>>
+        recv_types_span(recv_types_buffer.data(), recv_types_buffer.size() / 2,
+                        2);
+    {
+      for (std::size_t i = 0; i < recv_types_span.extent(0); ++i)
+        recv_types.push_back({recv_types_span(i, 0), recv_types_span(i, 1)});
+
+      std::ranges::sort(recv_types);
+      auto [unique_end, range_end] = std::ranges::unique(recv_types);
+      recv_types.erase(unique_end, range_end);
+    }
   }
 
-  // Map from VTKCellType to index in list of cell types
-  std::map<std::uint8_t, std::int32_t> type_to_index;
+  // Map from VTKCellType to index in list of (cell types, degree)
+  std::map<std::array<std::uint8_t, 2>, std::int32_t> type_to_index;
   std::vector<mesh::CellType> dolfinx_cell_type;
-  for (std::size_t i = 0; i < recv_types.size(); ++i)
-  {
-    type_to_index.insert({recv_types[i], i});
-    dolfinx_cell_type.push_back(vtk_to_dolfinx.at(recv_types[i]));
-  }
+  std::vector<std::uint8_t> dolfinx_cell_degree;
+  std::vector<std::uint32_t> num_nodes_per_cell;
+  dolfinx_cell_type.reserve(recv_types.size());
+  dolfinx_cell_degree.reserve(recv_types.size());
+  std::size_t c = 0;
+  std::for_each(recv_types.begin(), recv_types.end(),
+                [&type_to_index, &dolfinx_cell_type, &dolfinx_cell_degree,
+                 &vtk_to_dolfinx, &c](const auto& ct)
+                {
+                  mesh::CellType cell_type = vtk_to_dolfinx.at(ct.front());
+                  type_to_index.insert({ct, c++});
+                  dolfinx_cell_degree.push_back(ct.back());
+                  dolfinx_cell_type.push_back(cell_type);
+                });
 
   dset_id = io::hdf5::open_dataset(h5file, "/VTKHDF/NumberOfPoints");
   std::vector<std::int64_t> npoints
@@ -260,10 +313,17 @@ mesh::Mesh<U> io::VTKHDF::read_mesh(MPI_Comm comm, std::string filename)
   std::vector<U> points_local
       = io::hdf5::read_dataset<U>(dset_id, local_point_range, true);
   H5Dclose(dset_id);
-  dset_id = io::hdf5::open_dataset(h5file, "/VTKHDF/Offsets");
-  std::vector<std::int64_t> offsets = io::hdf5::read_dataset<std::int64_t>(
-      dset_id, {local_cell_range[0], local_cell_range[1] + 1}, true);
-  H5Dclose(dset_id);
+
+  // Remove coordinates if gdim!=3
+  assert(gdim < 4);
+  std::vector<U> points_pruned((local_point_range[1] - local_point_range[0])
+                               * gdim);
+  for (std::size_t i = 0; i < local_point_range[1] - local_point_range[0]; ++i)
+  {
+    std::copy_n(points_local.begin() + i * 3, gdim,
+                points_pruned.begin() + i * gdim);
+  }
+
   dset_id = io::hdf5::open_dataset(h5file, "/VTKHDF/Connectivity");
   std::vector<std::int64_t> topology = io::hdf5::read_dataset<std::int64_t>(
       dset_id, {offsets.front(), offsets.back()}, true);
@@ -277,25 +337,32 @@ mesh::Mesh<U> io::VTKHDF::read_mesh(MPI_Comm comm, std::string filename)
   std::vector<std::vector<std::int64_t>> cells_local(recv_types.size());
   for (std::size_t j = 0; j < types.size(); ++j)
   {
-    std::int32_t type_index = type_to_index.at(types[j]);
+    std::int32_t type_index = type_to_index.at({types[j], cell_degrees[j]});
     mesh::CellType cell_type = dolfinx_cell_type[type_index];
     auto perm = io::cells::perm_vtk(cell_type, offsets[j + 1] - offsets[j]);
-
     for (std::size_t k = 0; k < offsets[j + 1] - offsets[j]; ++k)
       cells_local[type_index].push_back(topology[perm[k] + offsets[j]]);
   }
 
-  // Make first order coordinate elements
+  // Make coordinate elements
   std::vector<fem::CoordinateElement<U>> coordinate_elements;
-  for (auto& cell : dolfinx_cell_type)
-    coordinate_elements.push_back(fem::CoordinateElement<U>(cell, 1));
+  std::transform(
+      dolfinx_cell_type.cbegin(), dolfinx_cell_type.cend(),
+      dolfinx_cell_degree.cbegin(), std::back_inserter(coordinate_elements),
+      [](const auto& cell_type, const auto& cell_degree)
+      {
+        basix::element::lagrange_variant variant
+            = basix::element::lagrange_variant::unset;
+        if (cell_degree > 2)
+          variant = basix::element::lagrange_variant::equispaced;
+        return fem::CoordinateElement<U>(cell_type, cell_degree, variant);
+      });
 
   auto part = create_cell_partitioner(mesh::GhostMode::none);
   std::vector<std::span<const std::int64_t>> cells_span;
   for (auto& cells : cells_local)
     cells_span.push_back(cells);
-  std::array<std::size_t, 2> xs
-      = {(std::size_t)x_shape[0], (std::size_t)x_shape[1]};
+  std::array<std::size_t, 2> xs = {(std::size_t)x_shape[0], (std::size_t)gdim};
   return create_mesh(comm, comm, cells_span, coordinate_elements, comm,
-                     points_local, xs, part);
+                     points_pruned, xs, part);
 }
diff --git a/cpp/dolfinx/io/cells.cpp b/cpp/dolfinx/io/cells.cpp
index 47682f7bfe0..750d42326da 100644
--- a/cpp/dolfinx/io/cells.cpp
+++ b/cpp/dolfinx/io/cells.cpp
@@ -20,83 +20,6 @@ using namespace dolfinx;
 namespace
 {
 //----------------------------------------------------------------------------
-int cell_degree(mesh::CellType type, int num_nodes)
-{
-  switch (type)
-  {
-  case mesh::CellType::point:
-    return 1;
-  case mesh::CellType::interval:
-    return num_nodes - 1;
-  case mesh::CellType::triangle:
-  {
-    const int n = (std::sqrt(1 + 8 * num_nodes) - 1) / 2;
-    if (2 * num_nodes != n * (n + 1))
-    {
-      throw std::runtime_error("Unknown triangle layout. Number of nodes: "
-                               + std::to_string(num_nodes));
-    }
-    return n - 1;
-  }
-  case mesh::CellType::tetrahedron:
-  {
-    int n = 0;
-    while (n * (n + 1) * (n + 2) < 6 * num_nodes)
-      ++n;
-    if (n * (n + 1) * (n + 2) != 6 * num_nodes)
-    {
-      throw std::runtime_error("Unknown tetrahedron layout. Number of nodes: "
-                               + std::to_string(num_nodes));
-    }
-    return n - 1;
-  }
-  case mesh::CellType::quadrilateral:
-  {
-    const int n = std::sqrt(num_nodes);
-    if (num_nodes != n * n)
-    {
-      throw std::runtime_error("Unknown quadrilateral layout. Number of nodes: "
-                               + std::to_string(num_nodes));
-    }
-    return n - 1;
-  }
-  case mesh::CellType::hexahedron:
-  {
-    const int n = std::cbrt(num_nodes);
-    if (num_nodes != n * n * n)
-    {
-      throw std::runtime_error("Unknown hexahedron layout. Number of nodes: "
-                               + std::to_string(num_nodes));
-    }
-    return n - 1;
-  }
-  case mesh::CellType::prism:
-    switch (num_nodes)
-    {
-    case 6:
-      return 1;
-    case 15:
-      return 2;
-    default:
-      throw std::runtime_error("Unknown prism layout. Number of nodes: "
-                               + std::to_string(num_nodes));
-    }
-  case mesh::CellType::pyramid:
-    switch (num_nodes)
-    {
-    case 5:
-      return 1;
-    case 13:
-      return 2;
-    default:
-      throw std::runtime_error("Unknown pyramid layout. Number of nodes: "
-                               + std::to_string(num_nodes));
-    }
-  default:
-    throw std::runtime_error("Unknown cell type.");
-  }
-}
-//----------------------------------------------------------------------------
 std::uint16_t vec_pop(std::vector<std::uint16_t>& v, int i)
 {
   auto pos = (i < 0) ? v.end() + i : v.begin() + i;
@@ -119,7 +42,8 @@ vtk_triangle_remainders(std::vector<std::uint16_t> remainders)
       break;
     }
 
-    int degree = cell_degree(mesh::CellType::triangle, remainders.size());
+    int degree
+        = io::cells::cell_degree(mesh::CellType::triangle, remainders.size());
 
     map.push_back(vec_pop(remainders, 0));
     map.push_back(vec_pop(remainders, degree - 1));
@@ -149,7 +73,7 @@ std::vector<std::uint16_t> vtk_triangle(int num_nodes)
   // Vertices
   map.insert(map.begin(), {0, 1, 2});
 
-  int degree = cell_degree(mesh::CellType::triangle, num_nodes);
+  int degree = io::cells::cell_degree(mesh::CellType::triangle, num_nodes);
   for (int k = 1; k < degree; ++k)
     map.push_back(3 + 2 * (degree - 1) + k - 1);
   for (int k = 1; k < degree; ++k)
@@ -184,7 +108,8 @@ vtk_tetrahedron_remainders(std::vector<std::uint16_t> remainders)
     }
 
     const int deg
-        = cell_degree(mesh::CellType::tetrahedron, remainders.size()) + 1;
+        = io::cells::cell_degree(mesh::CellType::tetrahedron, remainders.size())
+          + 1;
     map.push_back(vec_pop(remainders, 0));
     map.push_back(vec_pop(remainders, deg - 2));
     map.push_back(vec_pop(remainders, deg * (deg + 1) / 2 - 3));
@@ -330,7 +255,8 @@ vtk_tetrahedron_remainders(std::vector<std::uint16_t> remainders)
 //-----------------------------------------------------------------------------
 std::vector<std::uint16_t> vtk_tetrahedron(int num_nodes)
 {
-  const int degree = cell_degree(mesh::CellType::tetrahedron, num_nodes);
+  const int degree
+      = io::cells::cell_degree(mesh::CellType::tetrahedron, num_nodes);
 
   std::vector<std::uint16_t> map;
   map.reserve(num_nodes);
@@ -447,7 +373,8 @@ std::vector<std::uint16_t> vtk_quadrilateral(int num_nodes)
   // Vertices
   map.insert(map.begin(), {0, 1, 3, 2});
 
-  const int n = cell_degree(mesh::CellType::quadrilateral, num_nodes);
+  const int n
+      = io::cells::cell_degree(mesh::CellType::quadrilateral, num_nodes);
 
   // Edges
   const int edge_nodes = n - 1;
@@ -469,7 +396,8 @@ std::vector<std::uint16_t> vtk_quadrilateral(int num_nodes)
 //-----------------------------------------------------------------------------
 std::vector<std::uint16_t> vtk_hexahedron(int num_nodes)
 {
-  std::uint16_t n = cell_degree(mesh::CellType::hexahedron, num_nodes);
+  std::uint16_t n
+      = io::cells::cell_degree(mesh::CellType::hexahedron, num_nodes);
 
   std::vector<std::uint16_t> map;
   map.reserve(num_nodes);
@@ -661,6 +589,83 @@ std::vector<std::uint16_t> io::cells::perm_gmsh(mesh::CellType type,
   return cells::transpose(map);
 }
 //-----------------------------------------------------------------------------
+int io::cells::cell_degree(mesh::CellType type, int num_nodes)
+{
+  switch (type)
+  {
+  case mesh::CellType::point:
+    return 1;
+  case mesh::CellType::interval:
+    return num_nodes - 1;
+  case mesh::CellType::triangle:
+  {
+    const int n = (std::sqrt(1 + 8 * num_nodes) - 1) / 2;
+    if (2 * num_nodes != n * (n + 1))
+    {
+      throw std::runtime_error("Unknown triangle layout. Number of nodes: "
+                               + std::to_string(num_nodes));
+    }
+    return n - 1;
+  }
+  case mesh::CellType::tetrahedron:
+  {
+    int n = 0;
+    while (n * (n + 1) * (n + 2) < 6 * num_nodes)
+      ++n;
+    if (n * (n + 1) * (n + 2) != 6 * num_nodes)
+    {
+      throw std::runtime_error("Unknown tetrahedron layout. Number of nodes: "
+                               + std::to_string(num_nodes));
+    }
+    return n - 1;
+  }
+  case mesh::CellType::quadrilateral:
+  {
+    const int n = std::sqrt(num_nodes);
+    if (num_nodes != n * n)
+    {
+      throw std::runtime_error("Unknown quadrilateral layout. Number of nodes: "
+                               + std::to_string(num_nodes));
+    }
+    return n - 1;
+  }
+  case mesh::CellType::hexahedron:
+  {
+    const int n = std::cbrt(num_nodes);
+    if (num_nodes != n * n * n)
+    {
+      throw std::runtime_error("Unknown hexahedron layout. Number of nodes: "
+                               + std::to_string(num_nodes));
+    }
+    return n - 1;
+  }
+  case mesh::CellType::prism:
+    switch (num_nodes)
+    {
+    case 6:
+      return 1;
+    case 15:
+      return 2;
+    default:
+      throw std::runtime_error("Unknown prism layout. Number of nodes: "
+                               + std::to_string(num_nodes));
+    }
+  case mesh::CellType::pyramid:
+    switch (num_nodes)
+    {
+    case 5:
+      return 1;
+    case 13:
+      return 2;
+    default:
+      throw std::runtime_error("Unknown pyramid layout. Number of nodes: "
+                               + std::to_string(num_nodes));
+    }
+  default:
+    throw std::runtime_error("Unknown cell type.");
+  }
+}
+//-----------------------------------------------------------------------------
 std::vector<std::uint16_t>
 io::cells::transpose(std::span<const std::uint16_t> map)
 {
diff --git a/cpp/dolfinx/io/cells.h b/cpp/dolfinx/io/cells.h
index bbf89175ba8..119d0266db7 100644
--- a/cpp/dolfinx/io/cells.h
+++ b/cpp/dolfinx/io/cells.h
@@ -117,6 +117,15 @@ Pyramid:                     Pyramid13:
 */
 namespace dolfinx::io::cells
 {
+
+/// @brief Get the Lagrange order of a given cell with a given number of
+/// nodes.
+///
+/// @param[in] type Cell shape.
+/// @param[in] num_nodes Number of cell 'nodes'
+/// @return Lagrange order of the cell type.
+int cell_degree(mesh::CellType type, int num_nodes);
+
 /// @brief Permutation array to map from VTK to DOLFINx node ordering.
 ///
 /// @param[in] type Cell shape.
diff --git a/python/dolfinx/io/vtkhdf.py b/python/dolfinx/io/vtkhdf.py
index 4bba003a139..826f2579afe 100644
--- a/python/dolfinx/io/vtkhdf.py
+++ b/python/dolfinx/io/vtkhdf.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2024 Chris Richardson
+# Copyright (C) 2024-2025 Chris Richardson and Jørgen S. Dokken
 #
 # This file is part of DOLFINx (https://www.fenicsproject.org)
 #
@@ -19,18 +19,22 @@
 
 
 def read_mesh(
-    comm: _MPI.Comm, filename: typing.Union[str, Path], dtype: npt.DTypeLike = np.float64
+    comm: _MPI.Comm,
+    filename: typing.Union[str, Path],
+    dtype: npt.DTypeLike = np.float64,
+    gdim: int = 3,
 ):
     """Read a mesh from a VTKHDF format file
     Args:
            comm: An MPI communicator.
            filename: File to read from.
            dtype: Scalar type of mesh geometry (need not match dtype in file)
+           gdim: Geometric dimension of the mesh.
     """
     if dtype == np.float64:
-        mesh_cpp = read_vtkhdf_mesh_float64(comm, filename)
+        mesh_cpp = read_vtkhdf_mesh_float64(comm, filename, gdim)
     elif dtype == np.float32:
-        mesh_cpp = read_vtkhdf_mesh_float32(comm, filename)
+        mesh_cpp = read_vtkhdf_mesh_float32(comm, filename, gdim)
 
     cell_types = mesh_cpp.topology.entity_types[-1]
     if len(cell_types) > 1:
@@ -38,12 +42,13 @@ def read_mesh(
         domain = None
     else:
         cell_degree = mesh_cpp.geometry.cmap.degree
+        variant = mesh_cpp.geometry.cmap.variant
         domain = ufl.Mesh(
             basix.ufl.element(
                 "Lagrange",
                 cell_types[0].name,
                 cell_degree,
-                basix.LagrangeVariant.unset,
+                variant,
                 shape=(mesh_cpp.geometry.dim,),
             )
         )
diff --git a/python/dolfinx/wrappers/io.cpp b/python/dolfinx/wrappers/io.cpp
index f6362f714c9..82641ee68d0 100644
--- a/python/dolfinx/wrappers/io.cpp
+++ b/python/dolfinx/wrappers/io.cpp
@@ -220,12 +220,18 @@ void io(nb::module_& m)
 
   m.def("write_vtkhdf_mesh", &dolfinx::io::VTKHDF::write_mesh<double>)
       .def("write_vtkhdf_mesh", &dolfinx::io::VTKHDF::write_mesh<float>);
-  m.def(
-      "read_vtkhdf_mesh_float64", [](MPICommWrapper comm, std::string filename)
-      { return dolfinx::io::VTKHDF::read_mesh<double>(comm.get(), filename); });
-  m.def(
-      "read_vtkhdf_mesh_float32", [](MPICommWrapper comm, std::string filename)
-      { return dolfinx::io::VTKHDF::read_mesh<float>(comm.get(), filename); });
+  m.def("read_vtkhdf_mesh_float64",
+        [](MPICommWrapper comm, std::string filename, std::size_t gdim)
+        {
+          return dolfinx::io::VTKHDF::read_mesh<double>(comm.get(), filename,
+                                                        gdim);
+        });
+  m.def("read_vtkhdf_mesh_float32",
+        [](MPICommWrapper comm, std::string filename, std::size_t gdim)
+        {
+          return dolfinx::io::VTKHDF::read_mesh<float>(comm.get(), filename,
+                                                       gdim);
+        });
 
   // dolfinx::io::cell permutation functions
   m.def("perm_vtk", &dolfinx::io::cells::perm_vtk, nb::arg("type"),
diff --git a/python/test/unit/io/test_vtkhdf.py b/python/test/unit/io/test_vtkhdf.py
index be572513d36..a42beac8d56 100644
--- a/python/test/unit/io/test_vtkhdf.py
+++ b/python/test/unit/io/test_vtkhdf.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2024 Chris Richardson
+# Copyright (C) 2024-2025 Chris Richardson and Jørgen S. Dokken
 #
 # This file is part of DOLFINx (https://www.fenicsproject.org)
 #
@@ -7,7 +7,10 @@
 from mpi4py import MPI
 
 import numpy as np
+import pytest
 
+import dolfinx
+import ufl
 from dolfinx.io.vtkhdf import read_mesh, write_mesh
 from dolfinx.mesh import CellType, Mesh, create_unit_cube, create_unit_square
 
@@ -37,3 +40,120 @@ def test_read_write_mixed_topology(mixed_topology_mesh):
     mesh2 = read_mesh(MPI.COMM_WORLD, "mixed_mesh.vtkhdf", np.float64)
     for t in mesh2.topology.entity_types[-1]:
         assert t in mesh.topology.entity_types[-1]
+
+
+def test_read_write_higher_order():
+    # Create a simple, 2 cell mesh consisting of a second order quadrilateral and
+    # a second order triangle.
+    geom = np.array(
+        [
+            [0.0, 0.0],
+            [1.0, 0.0],
+            [1.0, 1.0],
+            [0.0, 1.0],
+            [0.5, 0],
+            [1, 0.5],
+            [0.5, 1],
+            [0, 0.5],
+            [0.5, 0.5],
+            [2.0, 0],
+            [1.5, -0.2],
+            [1.5, 0.6],
+        ],
+        dtype=np.float64,
+    )
+    # Nodes ordered as VTK
+    if MPI.COMM_WORLD.rank == 0:
+        topology_quad = np.array([[0, 1, 2, 3, 4, 5, 6, 7, 8]], dtype=np.int64)
+        topology_tri = np.array([[1, 9, 2, 10, 11, 5]], dtype=np.int64)
+
+    else:
+        topology_quad = np.empty((0, 9), dtype=np.int64)
+        topology_tri = np.empty((0, 6), dtype=np.int64)
+
+    quad_perm = dolfinx.io.utils.cell_perm_vtk(dolfinx.mesh.CellType.quadrilateral, 9)
+    tri_perm = dolfinx.io.utils.cell_perm_vtk(dolfinx.mesh.CellType.triangle, 6)
+    topology_quad = topology_quad[:, quad_perm]
+    topology_tri = topology_tri[:, tri_perm]
+
+    cells_np = [topology_quad.flatten(), topology_tri.flatten()]
+    coordinate_elements = [
+        dolfinx.fem.coordinate_element(cell, 2)
+        for cell in [dolfinx.mesh.CellType.quadrilateral, dolfinx.mesh.CellType.triangle]
+    ]
+
+    part = dolfinx.mesh.create_cell_partitioner(dolfinx.mesh.GhostMode.none)
+    mesh = dolfinx.cpp.mesh.create_mesh(
+        MPI.COMM_WORLD, cells_np, [e._cpp_object for e in coordinate_elements], geom, part
+    )
+    py_mesh = Mesh(mesh, None)
+
+    # Write mesh to file
+    write_mesh("mixed_mesh_second_order.vtkhdf", py_mesh)
+
+    # Read mesh as a 2D grid and a flat manifold in 3D
+    for gdim in [2, 3]:
+        mesh_in = read_mesh(MPI.COMM_WORLD, "mixed_mesh_second_order.vtkhdf", gdim=gdim)
+        assert mesh_in.geometry.dim == gdim
+        assert mesh_in.geometry.index_map().size_global == 12
+        cmap_0 = mesh_in.geometry._cpp_object.cmaps(0)
+        cmap_1 = mesh_in.geometry._cpp_object.cmaps(1)
+        assert cmap_0.degree == 2
+        assert cmap_1.degree == 2
+
+        cell_types = mesh.topology.cell_types
+        assert dolfinx.mesh.CellType.quadrilateral in cell_types
+        assert dolfinx.mesh.CellType.triangle in cell_types
+
+
+@pytest.mark.parametrize("order", [1, 2, 3])
+def test_read_write_higher_order_mesh(order):
+    try:
+        import gmsh
+    except ImportError:
+        pytest.skip()
+
+    # Create a tetrahedral mesh of a sphere
+    res = 0.3
+    gmsh.initialize()
+    comm = MPI.COMM_WORLD
+    rank = 0
+    model = None
+    gmsh.model.add(f"mesh_{order}")
+    if comm.rank == rank:
+        gmsh.option.setNumber("Mesh.CharacteristicLengthMin", res)
+        gmsh.option.setNumber("Mesh.CharacteristicLengthMax", res)
+        gmsh.model.occ.addSphere(0, 0, 0, 1, tag=1)
+        gmsh.model.occ.synchronize()
+        gmsh.model.addPhysicalGroup(3, [1], 1)
+        gmsh.model.mesh.generate(3)
+        gmsh.model.mesh.setOrder(order)
+    comm.Barrier()
+
+    model = comm.bcast(model, root=rank)
+    # Read in mesh with gmsh to create reference dat
+    ref_mesh = dolfinx.io.gmshio.model_to_mesh(gmsh.model, comm, rank).mesh
+    gmsh.finalize()
+
+    ref_volume_form = dolfinx.fem.form(1 * ufl.dx(domain=ref_mesh))
+    ref_volume = comm.allreduce(dolfinx.fem.assemble_scalar(ref_volume_form), op=MPI.SUM)
+
+    ref_surface_form = dolfinx.fem.form(1 * ufl.ds(domain=ref_mesh))
+    ref_surface = comm.allreduce(dolfinx.fem.assemble_scalar(ref_surface_form), op=MPI.SUM)
+
+    # Write to file
+    filename = f"gmsh_{order}_order_sphere.vtkhdf"
+    write_mesh(filename, ref_mesh)
+    del ref_mesh, ref_volume_form
+
+    # Read mesh
+    mesh = read_mesh(comm, filename)
+
+    # Compare surface and volume metrics
+    volume_form = dolfinx.fem.form(1 * ufl.dx(domain=mesh))
+    volume = comm.allreduce(dolfinx.fem.assemble_scalar(volume_form), op=MPI.SUM)
+    assert np.isclose(ref_volume, volume)
+
+    surface_form = dolfinx.fem.form(1 * ufl.ds(domain=mesh))
+    surface = comm.allreduce(dolfinx.fem.assemble_scalar(surface_form), op=MPI.SUM)
+    assert np.isclose(ref_surface, surface)


Reviews:
user: login: garth-wells
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T15:52:59Z
user: login: garth-wells
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T15:55:29Z
user: login: garth-wells
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T15:57:06Z
user: login: garth-wells
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T15:58:23Z
user: login: chrisrichardson
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T08:53:23Z
user: login: garth-wells
body: Needs some tests.

Would be good to make the title clearer - does it support write, read, or both?
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-04-08T09:07:14Z
user: login: jorgensd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T16:51:02Z
user: login: jorgensd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T17:06:44Z

Review comments:
diff_hunk: @@ -199,7 +200,40 @@ mesh::Mesh<U> io::VTKHDF::read_mesh(MPI_Comm comm, std::string filename)
   std::vector<std::uint8_t> types
       = io::hdf5::read_dataset<std::uint8_t>(dset_id, local_cell_range, true);
   H5Dclose(dset_id);
-  std::vector<std::uint8_t> types_unique(types.begin(), types.end());
+  std::vector<std::array<std::uint8_t, 2>> types_unique;
+  types_unique.reserve(types.size());
+
+  // Create reverse map from VTK to dolfinx cell types
+  std::map<std::uint8_t, mesh::CellType> vtk_to_dolfinx;
+  const std::vector<mesh::CellType> dolfinx_cells
path: cpp/dolfinx/io/VTKHDF.h
user: login: garth-wells
body: Use a `constexpr std::array` for `dolfinx_cells`
created_at: 2025-04-04T15:52:59Z
author_association: MEMBER
diff_hunk: @@ -215,36 +249,31 @@ mesh::Mesh<U> io::VTKHDF::read_mesh(MPI_Comm comm, std::string filename)
                 comm);
   std::partial_sum(recv_count.begin(), recv_count.end(),
                    recv_offsets.begin() + 1);
-  std::vector<std::uint8_t> recv_types(recv_offsets.back());
-  MPI_Allgatherv(types_unique.data(), count, MPI_UINT8_T, recv_types.data(),
-                 recv_count.data(), recv_offsets.data(), MPI_UINT8_T, comm);
+
+  MPI_Datatype compound_type;
+  MPI_Type_contiguous(2, MPI_UINT8_T, &compound_type);
+  MPI_Type_commit(&compound_type);
+
+  std::vector<std::array<std::uint8_t, 2>> recv_types(recv_offsets.back());
path: cpp/dolfinx/io/VTKHDF.h
user: login: garth-wells
body: This look very wrong. You need to use a flat `std::vector` with length `2 * recv_offsets.back()`.

Passing `recv_types.data()` is a pointer to the first `std::array`, rather than a pointer to the first `std::uint8_t`
created_at: 2025-04-04T15:55:29Z
author_association: MEMBER
diff_hunk: @@ -215,36 +249,31 @@ mesh::Mesh<U> io::VTKHDF::read_mesh(MPI_Comm comm, std::string filename)
                 comm);
   std::partial_sum(recv_count.begin(), recv_count.end(),
                    recv_offsets.begin() + 1);
-  std::vector<std::uint8_t> recv_types(recv_offsets.back());
-  MPI_Allgatherv(types_unique.data(), count, MPI_UINT8_T, recv_types.data(),
-                 recv_count.data(), recv_offsets.data(), MPI_UINT8_T, comm);
+
+  MPI_Datatype compound_type;
+  MPI_Type_contiguous(2, MPI_UINT8_T, &compound_type);
+  MPI_Type_commit(&compound_type);
+
+  std::vector<std::array<std::uint8_t, 2>> recv_types(recv_offsets.back());
+  MPI_Allgatherv(types_unique.data(), count, compound_type, recv_types.data(),
+                 recv_count.data(), recv_offsets.data(), compound_type, comm);
   {
     std::ranges::sort(recv_types);
     auto [unique_end, range_end] = std::ranges::unique(recv_types);
     recv_types.erase(unique_end, range_end);
   }
 
-  // Create reverse map from VTK to dolfinx cell types
-  std::map<std::uint8_t, mesh::CellType> vtk_to_dolfinx;
-  const std::vector<mesh::CellType> dolfinx_cells
-      = {mesh::CellType::point,       mesh::CellType::interval,
-         mesh::CellType::triangle,    mesh::CellType::quadrilateral,
-         mesh::CellType::tetrahedron, mesh::CellType::prism,
-         mesh::CellType::pyramid,     mesh::CellType::hexahedron};
-  for (auto dolfinx_type : dolfinx_cells)
-  {
-    vtk_to_dolfinx.insert({io::cells::get_vtk_cell_type(
-                               dolfinx_type, mesh::cell_dim(dolfinx_type)),
-                           dolfinx_type});
-  }
-
-  // Map from VTKCellType to index in list of cell types
-  std::map<std::uint8_t, std::int32_t> type_to_index;
+  // Map from VTKCellType to index in list of (cell types, degree)
+  std::map<std::array<std::uint8_t, 2>, std::int32_t> type_to_index;
   std::vector<mesh::CellType> dolfinx_cell_type;
+  std::vector<std::uint8_t> dolfinx_cell_degree;
+  std::vector<std::uint32_t> num_nodes_per_cell;
   for (std::size_t i = 0; i < recv_types.size(); ++i)
path: cpp/dolfinx/io/VTKHDF.h
user: login: garth-wells
body: Looks like a range-based loop could be used.
created_at: 2025-04-04T15:57:06Z
author_association: MEMBER
diff_hunk: @@ -277,18 +303,20 @@ mesh::Mesh<U> io::VTKHDF::read_mesh(MPI_Comm comm, std::string filename)
   std::vector<std::vector<std::int64_t>> cells_local(recv_types.size());
   for (std::size_t j = 0; j < types.size(); ++j)
   {
-    std::int32_t type_index = type_to_index.at(types[j]);
+    std::int32_t type_index = type_to_index.at({types[j], cell_degrees[j]});
     mesh::CellType cell_type = dolfinx_cell_type[type_index];
     auto perm = io::cells::perm_vtk(cell_type, offsets[j + 1] - offsets[j]);
-
     for (std::size_t k = 0; k < offsets[j + 1] - offsets[j]; ++k)
       cells_local[type_index].push_back(topology[perm[k] + offsets[j]]);
   }
 
-  // Make first order coordinate elements
+  // Make coordinate elements
   std::vector<fem::CoordinateElement<U>> coordinate_elements;
-  for (auto& cell : dolfinx_cell_type)
-    coordinate_elements.push_back(fem::CoordinateElement<U>(cell, 1));
+  for (std::size_t i = 0; i < dolfinx_cell_type.size(); ++i)
path: cpp/dolfinx/io/VTKHDF.h
user: login: garth-wells
body: Could use `std::transform` here.
created_at: 2025-04-04T15:58:23Z
author_association: MEMBER
diff_hunk: @@ -29,9 +29,10 @@ void write_mesh(std::string filename, const mesh::Mesh<U>& mesh);
 /// @tparam U Scalar type of mesh
 /// @param comm MPI Communicator for reading mesh
 /// @param filename Filename
+/// @param gdim Geometric dimension
 /// @return a mesh
 template <typename U>
-mesh::Mesh<U> read_mesh(MPI_Comm comm, std::string filename);
+mesh::Mesh<U> read_mesh(MPI_Comm comm, std::string filename.std::size_t gdim);
path: cpp/dolfinx/io/VTKHDF.h
user: login: jorgensd
body: ```suggestion
mesh::Mesh<U> read_mesh(MPI_Comm comm, std::string filename, std::size_t gdim);
```
created_at: 2025-04-08T16:51:01Z
author_association: MEMBER
diff_hunk: @@ -29,9 +29,10 @@ void write_mesh(std::string filename, const mesh::Mesh<U>& mesh);
 /// @tparam U Scalar type of mesh
 /// @param comm MPI Communicator for reading mesh
 /// @param filename Filename
+/// @param gdim Geometric dimension
 /// @return a mesh
 template <typename U>
-mesh::Mesh<U> read_mesh(MPI_Comm comm, std::string filename);
+mesh::Mesh<U> read_mesh(MPI_Comm comm, std::string filename, std::size_t gdim);
path: cpp/dolfinx/io/VTKHDF.h
user: login: jorgensd
body: ```suggestion
mesh::Mesh<U> read_mesh(MPI_Comm comm, std::string filename, std::size_t gdim = 3);
```
created_at: 2025-04-08T17:06:44Z
author_association: MEMBER

PR comments:
user: login: jorgensd
created_at: 2025-04-08T10:22:45Z
body: > Needs some tests.
> 
> Would be good to make the title clearer - does it support write, read, or both?

Do you have any good examples at hand for mixed meshes?
I can hand-code a 2D higher order mixed mesh (1 second order triangle + 1 second order quadrilateral).

The code supports read and write.

author_association: MEMBER
user: login: chrisrichardson
created_at: 2025-04-08T10:40:57Z
body: There is a mixed mesh fixture (see last test in `io/test_vtkdhf.py`) although this is only first-order.
author_association: CONTRIBUTOR
user: login: garth-wells
created_at: 2025-04-08T11:09:41Z
body: Single cell type tests would be enough for now.
author_association: MEMBER
user: login: jorgensd
created_at: 2025-04-08T14:52:33Z
body: Tests for 1st, 2nd and 3rd order single cell meshes (tetrahedra) have been added, as well as a hand-coded 2-element mesh (2nd order triangle, 2nd quadrilateral).
They exposed a few flaws in the original design, including:
- VTKHDF doesn't have a notion of 2D grids (we therefore need to supply gdim (as we do when reading gmsh meshes) if we want a 1D or 2D grid
- For higher order grids (degree >2) a Lagrange type is needed. It has now been hardcoded to equispaced both in Python and C++, for consistency and is similar to what we do for GMSH grids.
author_association: MEMBER