From e46269fee2e26acd83a863a31d2f2af67da2dc4a Mon Sep 17 00:00:00 2001 From: Guillaume Giudicelli Date: Wed, 16 Sep 2026 17:23:53 -0600 Subject: [PATCH 1/4] Read VTK_POLYHEDRON cells in the VTU reader refs #4555 VTKIO::read() now handles VTK_POLYHEDRON cells, which VTK describes by a face stream rather than an ordered node list. Each such cell is built as a C0Polyhedron from C0Polygon faces (obtained via vtkUnstructuredGrid::GetFaceStream), mirroring the Exodus NFACED reader; the interior mid-element node the C0Polyhedron construction may create is added to the mesh. Adds a testVTKReadPolyhedra unit test that reads a hexagonal-prism polyhedron .vtu fixture and checks the element type, side and face connectivity. Co-Authored-By: Claude --- src/mesh/vtk_io.C | 121 ++++++++++++++++++++++---- tests/mesh/mesh_input.C | 57 ++++++++++++ tests/meshes/hex_prism_polyhedron.vtu | 38 ++++++++ 3 files changed, 197 insertions(+), 19 deletions(-) create mode 100644 tests/meshes/hex_prism_polyhedron.vtu diff --git a/src/mesh/vtk_io.C b/src/mesh/vtk_io.C index d479c6e4c54..5a6fd3e95a4 100644 --- a/src/mesh/vtk_io.C +++ b/src/mesh/vtk_io.C @@ -25,6 +25,9 @@ #include "libmesh/system.h" #include "libmesh/node.h" #include "libmesh/elem.h" +#include "libmesh/cell_c0polyhedron.h" +#include "libmesh/face_c0polygon.h" +#include "libmesh/face_polygon.h" #include "libmesh/enum_io_package.h" #include "libmesh/utility.h" @@ -44,6 +47,7 @@ #include "vtkCellData.h" #include "vtkDoubleArray.h" #include "vtkGenericCell.h" +#include "vtkIdList.h" #include "vtkPointData.h" #include "vtkPoints.h" #include "vtkSmartPointer.h" @@ -183,6 +187,72 @@ std::map VTKIO::build_element_maps() +namespace { + +// VTK describes a polyhedron by a "face stream" (a list of polygonal +// faces, each a list of global point ids) rather than by the flat, +// ordered node list used for every other supported element type. This +// helper builds an equivalent libMesh C0Polyhedron from that face +// stream, assigning its node pointers and adding to \p mesh any interior +// "mid-element" node the C0Polyhedron construction requires. +std::unique_ptr +add_vtk_polyhedron(vtkUnstructuredGrid & vtk_grid, + MeshBase & mesh, + const vtkIdType cell_id, + vtkIntArray * node_id, + const std::vector & vtk_node_to_libmesh) +{ + // GetFaceStream() fills face_stream with + // [ n_faces, + // n_face0_pts, id0, id1, ..., + // n_face1_pts, id0, id1, ..., ] + // where all the point ids are global VTK point ids. + vtkSmartPointer face_stream = vtkSmartPointer::New(); + vtk_grid.GetFaceStream(cell_id, face_stream); + + vtkIdType pos = 0; + const vtkIdType n_faces = face_stream->GetId(pos++); + + libmesh_error_msg_if + (n_faces < 4, + "Error: VTK polyhedron cell " << cell_id << " has only " << n_faces << + " faces, but a polyhedron requires at least 4."); + + std::vector> sides(cast_int(n_faces)); + for (std::size_t s = 0; s != sides.size(); ++s) + { + const vtkIdType n_face_pts = face_stream->GetId(pos++); + auto side = std::make_shared(cast_int(n_face_pts)); + + for (vtkIdType n = 0; n != n_face_pts; ++n) + { + const vtkIdType vtk_point_id = face_stream->GetId(pos++); + const dof_id_type libmesh_node_id = node_id ? + vtk_node_to_libmesh[vtk_point_id] : + cast_int(vtk_point_id); + side->set_node(cast_int(n), + mesh.node_ptr(libmesh_node_id)); + } + + sides[s] = std::move(side); + } + + // Constructing the C0Polyhedron may create an interior "mid-element" + // node for its default tetrahedralization; if so, it is our job to + // add it to the mesh. The polyhedron's vertex node pointers were + // already assigned above via its polygonal sides. + std::unique_ptr mid_elem_node; + auto elem = std::make_unique(sides, mid_elem_node); + if (mid_elem_node) + mesh.add_node(std::move(mid_elem_node)); + + return elem; +} + +} // anonymous namespace + + + void VTKIO::read (const std::string & name) { // This is a serial-only process for now; @@ -286,30 +356,43 @@ void VTKIO::read (const std::string & name) { _vtk_grid->GetCell(i, cell); - // Get the libMesh element type corresponding to this VTK element type. - ElemType libmesh_elem_type = element_map.find(cell->GetCellType()); - auto elem = Elem::build(libmesh_elem_type); + std::unique_ptr elem; - // get the straightforward numbering from the VTK cells - for (auto j : elem->node_index_range()) + // VTK polyhedra are described by a stream of polygonal faces + // rather than by an ordered node list, so they are built + // separately as C0Polyhedron elements with their nodes already + // assigned via their sides. + if (cell->GetCellType() == VTK_POLYHEDRON) + elem = add_vtk_polyhedron(*_vtk_grid, mesh, + static_cast(i), + node_id, vtk_node_to_libmesh); + else { - const auto vtk_point_id = cell->GetPointId(j); - const dof_id_type libmesh_node_id = node_id ? - vtk_node_to_libmesh[vtk_point_id] : vtk_point_id; + // Get the libMesh element type corresponding to this VTK element type. + ElemType libmesh_elem_type = element_map.find(cell->GetCellType()); + elem = Elem::build(libmesh_elem_type); - elem->set_node(j, mesh.node_ptr(libmesh_node_id)); - } + // get the straightforward numbering from the VTK cells + for (auto j : elem->node_index_range()) + { + const auto vtk_point_id = cell->GetPointId(j); + const dof_id_type libmesh_node_id = node_id ? + vtk_node_to_libmesh[vtk_point_id] : vtk_point_id; - // then get the connectivity - std::vector conn; - elem->connectivity(0, VTK, conn); + elem->set_node(j, mesh.node_ptr(libmesh_node_id)); + } - // then reshuffle the nodes according to the connectivity, this - // two-time-assign would evade the definition of the vtk_mapping - for (unsigned int j=0, - n_conn = cast_int(conn.size()); - j != n_conn; ++j) - elem->set_node(j, mesh.node_ptr(conn[j])); + // then get the connectivity + std::vector conn; + elem->connectivity(0, VTK, conn); + + // then reshuffle the nodes according to the connectivity, this + // two-time-assign would evade the definition of the vtk_mapping + for (unsigned int j=0, + n_conn = cast_int(conn.size()); + j != n_conn; ++j) + elem->set_node(j, mesh.node_ptr(conn[j])); + } if (elem_id) { diff --git a/tests/mesh/mesh_input.C b/tests/mesh/mesh_input.C index 8dd2f8cbe00..07c25df4ddc 100644 --- a/tests/mesh/mesh_input.C +++ b/tests/mesh/mesh_input.C @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -104,6 +105,7 @@ public: #ifdef LIBMESH_HAVE_VTK CPPUNIT_TEST( testVTKPreserveElemIds ); CPPUNIT_TEST( testVTKPreserveSubdomainIds ); + CPPUNIT_TEST( testVTKReadPolyhedra ); #endif #ifdef LIBMESH_HAVE_EXODUS_API @@ -331,6 +333,61 @@ public: } } } + + void testVTKReadPolyhedra () + { + LOG_UNIT_TEST; + + // This .vtu file contains a single VTK_POLYHEDRON cell: a hexagonal + // prism with 12 vertices and 8 faces (two hexagons and six quads). + Mesh mesh(*TestCommWorld); + mesh.read("meshes/hex_prism_polyhedron.vtu"); + mesh.prepare_for_use(); + + CPPUNIT_ASSERT_EQUAL(dof_id_type(1), mesh.n_elem()); + + // The element may live on a single processor when the mesh is + // distributed, so guard the checks accordingly. + const Elem * elem = mesh.query_elem_ptr(0); + bool found_elem = elem; + mesh.comm().max(found_elem); + CPPUNIT_ASSERT(found_elem); + + if (!elem) + return; + + CPPUNIT_ASSERT_EQUAL(C0POLYHEDRON, elem->type()); + CPPUNIT_ASSERT_EQUAL(12u, elem->n_vertices()); + CPPUNIT_ASSERT_EQUAL(8u, elem->n_sides()); + + // The file has no libmesh_node_id array, so libMesh node ids match + // the VTK point ordering, i.e. the coordinates we wrote out. + LIBMESH_ASSERT_FP_EQUAL(6.0, elem->volume(), TOLERANCE); + + // Collect the faces (as node-id sets) and compare against the faces + // that were written to the file, independent of any reordering the + // C0Polyhedron may do to its sides. + std::set> faces; + for (auto s : elem->side_index_range()) + { + std::set face; + for (const auto n : elem->nodes_on_side(s)) + face.insert(elem->node_id(n)); + faces.insert(std::move(face)); + } + + const std::set> expected_faces = + { {0, 1, 2, 3, 4, 5}, + {0, 1, 7, 6}, + {1, 2, 8, 7}, + {2, 3, 9, 8}, + {3, 4, 10, 9}, + {4, 5, 11, 10}, + {5, 0, 6, 11}, + {6, 7, 8, 9, 10, 11} }; + + CPPUNIT_ASSERT(faces == expected_faces); + } #endif // LIBMESH_HAVE_VTK diff --git a/tests/meshes/hex_prism_polyhedron.vtu b/tests/meshes/hex_prism_polyhedron.vtu new file mode 100644 index 00000000000..a3d82e21d2d --- /dev/null +++ b/tests/meshes/hex_prism_polyhedron.vtu @@ -0,0 +1,38 @@ + + + + + + + 0 -2 0 -1 -1 0 -1 1 0 0 2 0 1 1 0 1 -1 0 + 0 -2 1 -1 -1 1 -1 1 1 0 2 1 1 1 1 1 -1 1 + + + + + 0 1 2 3 4 5 6 7 8 9 10 11 + + + 12 + + + 42 + + + 8 + 6 0 1 2 3 4 5 + 4 0 1 7 6 + 4 1 2 8 7 + 4 2 3 9 8 + 4 3 4 10 9 + 4 4 5 11 10 + 4 5 0 6 11 + 6 6 7 8 9 10 11 + + + 45 + + + + + From 69af8b2274765a54e783b9332f07f9ec483df342 Mon Sep 17 00:00:00 2001 From: Guillaume Giudicelli Date: Tue, 29 Sep 2026 15:16:34 -0600 Subject: [PATCH 2/4] Fix VTU polyhedron test fixture; error clearly on the wrong VTK file type CI hit "Cannot find PUnstructuredGrid element in file." from vtkXMLPUnstructuredGridReader. VTKIO::read() always uses that *parallel* XML reader class, which requires a genuine PUnstructuredGrid file (a .pvtu descriptor plus its piece file(s)) -- a plain serial UnstructuredGrid .vtu file, which is what the previous hex_prism_polyhedron.vtu fixture actually was, is not sufficient even for a single piece. Replace it with a real .pvtu + piece file pair (in the older, more broadly-supported "faces"/"faceoffsets" polyhedron schema rather than VTK 9.7's newer default), and update the test to read the .pvtu. Given the wrong file, vtkXMLPUnstructuredGridReader logs an error but otherwise fails "successfully," leaving VTKIO::read() with a silently empty grid rather than any clear diagnostic -- which is how this surfaced as a confusing assertion failure rather than an obvious one. Sniff the file's actual root attribute with vtkXMLFileReadTester before using it, and error out clearly if it isn't a PUnstructuredGrid. Also add the missing mesh.allow_renumbering(false) to testVTKReadPolyhedra (present in every sibling VTK test in this file, but missing here): without it, prepare_for_use()'s default renumbering can reassign node ids by local element-traversal order rather than preserving the ids read from the file, which would have made the test's per-face node-id checks fail (independent of, and unrelated to, the fixture-format bug above) once that was fixed. List the new fixture files in tests/Makefile.am's EXTRA_DIST, matching the existing meshes/ entries (the previous .vtu fixture was missing from that list too). Co-Authored-By: Claude --- src/mesh/vtk_io.C | 28 +++++++++++++++++++ tests/Makefile.am | 2 ++ tests/mesh/mesh_input.C | 14 ++++++++-- tests/meshes/hex_prism_polyhedron.pvtu | 9 ++++++ ...yhedron.vtu => hex_prism_polyhedron_0.vtu} | 0 5 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 tests/meshes/hex_prism_polyhedron.pvtu rename tests/meshes/{hex_prism_polyhedron.vtu => hex_prism_polyhedron_0.vtu} (100%) diff --git a/src/mesh/vtk_io.C b/src/mesh/vtk_io.C index 5a6fd3e95a4..cead35b2545 100644 --- a/src/mesh/vtk_io.C +++ b/src/mesh/vtk_io.C @@ -41,6 +41,7 @@ #include "vtkXMLPUnstructuredGridReader.h" #include "vtkXMLUnstructuredGridWriter.h" #include "vtkXMLPUnstructuredGridWriter.h" +#include "vtkXMLFileReadTester.h" #include "vtkUnstructuredGrid.h" #include "vtkIntArray.h" #include "vtkCellArray.h" @@ -61,6 +62,7 @@ #include "libmesh/restore_warnings.h" // C++ includes +#include #include @@ -264,6 +266,32 @@ void VTKIO::read (const std::string & name) elems_of_dimension.clear(); elems_of_dimension.resize(4, false); + // vtkXMLPUnstructuredGridReader is a *parallel*-format reader: it + // requires a real "PUnstructuredGrid" file (a .pvtu descriptor plus its + // piece file(s)), not a plain serial "UnstructuredGrid" .vtu file. Fed + // the wrong format, it logs an error but otherwise fails silently, + // leaving us with an empty grid rather than a clear diagnostic. Sniff + // the actual "type" attribute of the file's root element + // first so we can give a useful error instead. + { + vtkSmartPointer tester = + vtkSmartPointer::New(); + tester->SetFileName(name.c_str()); + + libmesh_error_msg_if + (!tester->TestReadFile(), + "Error: " << name << " does not appear to be a VTK XML file."); + + const char * file_type = tester->GetFileDataType(); + libmesh_error_msg_if + (!file_type || std::strcmp(file_type, "PUnstructuredGrid") != 0, + "Error: " << name << " is a VTK XML file of type \"" + << (file_type ? file_type : "(unknown)") << "\", but libMesh's " + "VTKIO reader requires a *parallel* unstructured grid file " + "(type=\"PUnstructuredGrid\", conventionally named .pvtu), with its " + "accompanying piece file(s), even for a single-processor mesh."); + } + // Use a typedef, because these names are just crazy typedef vtkSmartPointer MyReader; MyReader reader = MyReader::New(); diff --git a/tests/Makefile.am b/tests/Makefile.am index 4abebf8114f..d37b168fa69 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -230,6 +230,8 @@ data = matrices/geom_1_extraction_op.h5 \ meshes/quad4_tri3_smoothed.xda.gz \ meshes/quad4_tri3_hourglass.xda.gz \ meshes/hex8_prism6_smoothed.xda.gz \ + meshes/hex_prism_polyhedron.pvtu \ + meshes/hex_prism_polyhedron_0.vtu \ solutions/lagrange_vec_solution_mesh.xda \ solutions/lagrange_vec_solution.xda \ solutions/nedelec_one_solution_mesh.xda \ diff --git a/tests/mesh/mesh_input.C b/tests/mesh/mesh_input.C index 07c25df4ddc..42ac20139e5 100644 --- a/tests/mesh/mesh_input.C +++ b/tests/mesh/mesh_input.C @@ -338,10 +338,18 @@ public: { LOG_UNIT_TEST; - // This .vtu file contains a single VTK_POLYHEDRON cell: a hexagonal - // prism with 12 vertices and 8 faces (two hexagons and six quads). + // This .pvtu (+ piece) file contains a single VTK_POLYHEDRON cell: a + // hexagonal prism with 12 vertices and 8 faces (two hexagons and six + // quads). VTKIO::read() always uses the *parallel* XML reader, which + // requires a genuine PUnstructuredGrid (.pvtu) file -- a plain serial + // UnstructuredGrid .vtu file is not sufficient, even for one piece. Mesh mesh(*TestCommWorld); - mesh.read("meshes/hex_prism_polyhedron.vtu"); + // Without this, prepare_for_use()'s default renumbering can reassign + // node ids (by local element-traversal order rather than preserving + // the ids we read), which would invalidate the "ids match VTK point + // ordering" assumption the face checks below rely on. + mesh.allow_renumbering(false); + mesh.read("meshes/hex_prism_polyhedron.pvtu"); mesh.prepare_for_use(); CPPUNIT_ASSERT_EQUAL(dof_id_type(1), mesh.n_elem()); diff --git a/tests/meshes/hex_prism_polyhedron.pvtu b/tests/meshes/hex_prism_polyhedron.pvtu new file mode 100644 index 00000000000..cc88c97c1a9 --- /dev/null +++ b/tests/meshes/hex_prism_polyhedron.pvtu @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tests/meshes/hex_prism_polyhedron.vtu b/tests/meshes/hex_prism_polyhedron_0.vtu similarity index 100% rename from tests/meshes/hex_prism_polyhedron.vtu rename to tests/meshes/hex_prism_polyhedron_0.vtu From 02cd75dbcb564c1938f5bff30fb69383c2f4845a Mon Sep 17 00:00:00 2001 From: Guillaume Giudicelli Date: Tue, 29 Sep 2026 15:36:20 -0600 Subject: [PATCH 3/4] Add unit tests for the VTU wrong-file-type errors The previous commit added a check in VTKIO::read() that errors clearly when handed a VTK XML file that isn't a PUnstructuredGrid (rather than silently producing an empty mesh), but added no test for it. Add testVTKReadWrongFileType (the exact mistake that motivated the check: reading a piece file directly instead of its .pvtu descriptor) and testVTKReadNotAnXMLFile (not a VTK XML file at all, reusing an existing unrelated fixture). Both construct VTKIO directly rather than going through mesh.read(): that dispatches via NameBasedIO's rank-0-reads-then-broadcasts pattern, and throwing out of the read on rank 0 before the broadcast would leave every other rank blocked on a broadcast that never arrives. Calling VTKIO::read() directly makes every rank hit the same, rank-independent error with no such hazard -- verified by running both under mpirun with multiple ranks and confirming every rank reaches completion independently. Co-Authored-By: Claude --- tests/mesh/mesh_input.C | 49 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/mesh/mesh_input.C b/tests/mesh/mesh_input.C index 42ac20139e5..9f7dce70e28 100644 --- a/tests/mesh/mesh_input.C +++ b/tests/mesh/mesh_input.C @@ -106,6 +106,8 @@ public: CPPUNIT_TEST( testVTKPreserveElemIds ); CPPUNIT_TEST( testVTKPreserveSubdomainIds ); CPPUNIT_TEST( testVTKReadPolyhedra ); + CPPUNIT_TEST( testVTKReadWrongFileType ); + CPPUNIT_TEST( testVTKReadNotAnXMLFile ); #endif #ifdef LIBMESH_HAVE_EXODUS_API @@ -396,6 +398,53 @@ public: CPPUNIT_ASSERT(faces == expected_faces); } + + // VTKIO::read() always uses vtkXMLPUnstructuredGridReader, a *parallel* + // XML reader that requires a genuine PUnstructuredGrid (.pvtu) file. + // Handed the underlying serial UnstructuredGrid piece file directly -- + // an easy mistake, since it's a perfectly valid VTK XML file, just not + // the type this reader needs -- it used to fail silently (producing an + // empty mesh with no diagnostic at all) rather than erroring clearly. + // + // We construct VTKIO directly here rather than going through + // mesh.read(), which would dispatch via NameBasedIO's rank-0-reads/ + // then-broadcast pattern: throwing out of that on rank 0 before the + // broadcast would leave every other rank blocked on a broadcast that + // never arrives. Every rank hits this same, rank-independent error + // when calling VTKIO::read() directly, so there's no such hazard here. + void testVTKReadWrongFileType () + { + LOG_UNIT_TEST; + +#ifdef LIBMESH_ENABLE_EXCEPTIONS + Mesh mesh(*TestCommWorld); + VTKIO vtk(mesh); + CPPUNIT_ASSERT_THROW_MESSAGE + ("Wrong VTK file type (serial UnstructuredGrid, not " + "PUnstructuredGrid) not detected", + vtk.read("meshes/hex_prism_polyhedron_0.vtu"), + libMesh::LogicError); +#endif + } + + // A file that isn't a VTK XML file at all (wrong format entirely, or a + // corrupted/truncated download) should also be rejected clearly. We + // reuse an existing, unrelated mesh fixture here rather than adding a + // new one; any non-VTK-XML file demonstrates this code path. See + // testVTKReadWrongFileType above for why VTKIO is constructed directly. + void testVTKReadNotAnXMLFile () + { + LOG_UNIT_TEST; + +#ifdef LIBMESH_ENABLE_EXCEPTIONS + Mesh mesh(*TestCommWorld); + VTKIO vtk(mesh); + CPPUNIT_ASSERT_THROW_MESSAGE + ("Non-VTK-XML file not detected", + vtk.read("meshes/circle.msh"), + libMesh::LogicError); +#endif + } #endif // LIBMESH_HAVE_VTK From 6b9c53a866e68620cc4695c71cf40aae6f9fae1a Mon Sep 17 00:00:00 2001 From: Guillaume Giudicelli Date: Tue, 29 Sep 2026 18:31:57 -0600 Subject: [PATCH 4/4] VTKIO::read(): actually support plain serial .vtu files again The previous two commits treated "VTKIO::read() can only read .pvtu" as an inherent constraint to document and error around. It isn't: git history shows VTKIO::read() used vtkXMLUnstructuredGridReader (the *serial* reader) up until commit 7d4e02511f ("Support parallel .pvtu in VTKIO::read()", Jan 2024), whose entire 3-line diff swapped the reader class outright to add .pvtu support -- silently regressing plain .vtu support as a side effect (the serial reader's now-unused #include was even left dangling ever since). That regression is what actually broke CI: the original test fixture was a perfectly ordinary serial .vtu file. Fix it properly instead: detect the file's actual root element via vtkXMLFileReadTester (already added in the previous commit) and dispatch to whichever reader class matches -- vtkXMLUnstructuredGridReader for a plain "UnstructuredGrid" file, vtkXMLPUnstructuredGridReader for a "PUnstructuredGrid" one -- restoring serial support alongside the parallel support that motivated the original change. A file that is VTK XML but of neither type, or not VTK XML at all, still errors clearly (unchanged from the previous commit). Update the tests to match: testVTKReadWrongFileType's premise (that reading the serial piece file directly should throw) is no longer correct, since that is now a supported, ordinary thing to do. Replace it with testVTKReadPolyhedraSerial, a positive regression test asserting that this exact scenario now succeeds and reads the polyhedron correctly -- sharing its checks with testVTKReadPolyhedra via a new checkHexPrismPolyhedron() helper. testVTKReadNotAnXMLFile is unaffected and still passes as-is. Co-Authored-By: Claude --- src/mesh/vtk_io.C | 61 +++++++++++++++----------- tests/mesh/mesh_input.C | 97 ++++++++++++++++++++++------------------- 2 files changed, 88 insertions(+), 70 deletions(-) diff --git a/src/mesh/vtk_io.C b/src/mesh/vtk_io.C index cead35b2545..85e684aa693 100644 --- a/src/mesh/vtk_io.C +++ b/src/mesh/vtk_io.C @@ -266,13 +266,16 @@ void VTKIO::read (const std::string & name) elems_of_dimension.clear(); elems_of_dimension.resize(4, false); - // vtkXMLPUnstructuredGridReader is a *parallel*-format reader: it - // requires a real "PUnstructuredGrid" file (a .pvtu descriptor plus its - // piece file(s)), not a plain serial "UnstructuredGrid" .vtu file. Fed - // the wrong format, it logs an error but otherwise fails silently, - // leaving us with an empty grid rather than a clear diagnostic. Sniff - // the actual "type" attribute of the file's root element - // first so we can give a useful error instead. + // VTK XML unstructured grid files come in two root-element forms: a + // plain serial "UnstructuredGrid" file (conventionally .vtu), and a + // "PUnstructuredGrid" descriptor (conventionally .pvtu) referencing one + // or more serial piece files for parallel use. Each needs a different + // reader class. Sniff the file's actual root + // attribute rather than assuming one from the extension (or, worse, + // always assuming one -- vtkXMLPUnstructuredGridReader silently yields + // an empty grid, rather than any diagnostic, when handed the other + // format). + std::string file_type; { vtkSmartPointer tester = vtkSmartPointer::New(); @@ -282,28 +285,34 @@ void VTKIO::read (const std::string & name) (!tester->TestReadFile(), "Error: " << name << " does not appear to be a VTK XML file."); - const char * file_type = tester->GetFileDataType(); + const char * file_type_cstr = tester->GetFileDataType(); libmesh_error_msg_if - (!file_type || std::strcmp(file_type, "PUnstructuredGrid") != 0, - "Error: " << name << " is a VTK XML file of type \"" - << (file_type ? file_type : "(unknown)") << "\", but libMesh's " - "VTKIO reader requires a *parallel* unstructured grid file " - "(type=\"PUnstructuredGrid\", conventionally named .pvtu), with its " - "accompanying piece file(s), even for a single-processor mesh."); + (!file_type_cstr, + "Error: " << name << " does not specify a VTK XML data type."); + file_type = file_type_cstr; } - // Use a typedef, because these names are just crazy - typedef vtkSmartPointer MyReader; - MyReader reader = MyReader::New(); - - // Pass the filename along to the reader - reader->SetFileName(name.c_str()); - - // Force reading - reader->Update(); - - // read in the grid - _vtk_grid = reader->GetOutput(); + if (file_type == "PUnstructuredGrid") + { + vtkSmartPointer reader = + vtkSmartPointer::New(); + reader->SetFileName(name.c_str()); + reader->Update(); + _vtk_grid = reader->GetOutput(); + } + else if (file_type == "UnstructuredGrid") + { + vtkSmartPointer reader = + vtkSmartPointer::New(); + reader->SetFileName(name.c_str()); + reader->Update(); + _vtk_grid = reader->GetOutput(); + } + else + libmesh_error_msg + ("Error: " << name << " is a VTK XML file of type \"" << file_type + << "\", but libMesh's VTKIO reader only supports \"UnstructuredGrid\" " + "(.vtu) and \"PUnstructuredGrid\" (.pvtu) files."); // Get a reference to the mesh MeshBase & mesh = MeshInput::mesh(); diff --git a/tests/mesh/mesh_input.C b/tests/mesh/mesh_input.C index 9f7dce70e28..59d8f6ccf2e 100644 --- a/tests/mesh/mesh_input.C +++ b/tests/mesh/mesh_input.C @@ -106,7 +106,7 @@ public: CPPUNIT_TEST( testVTKPreserveElemIds ); CPPUNIT_TEST( testVTKPreserveSubdomainIds ); CPPUNIT_TEST( testVTKReadPolyhedra ); - CPPUNIT_TEST( testVTKReadWrongFileType ); + CPPUNIT_TEST( testVTKReadPolyhedraSerial ); CPPUNIT_TEST( testVTKReadNotAnXMLFile ); #endif @@ -336,24 +336,11 @@ public: } } - void testVTKReadPolyhedra () + // Shared checks for a mesh that should contain exactly the hexagonal + // prism polyhedron (12 vertices, 8 faces -- two hexagons and six + // quads) written to meshes/hex_prism_polyhedron.{pvtu,_0.vtu}. + void checkHexPrismPolyhedron (MeshBase & mesh) { - LOG_UNIT_TEST; - - // This .pvtu (+ piece) file contains a single VTK_POLYHEDRON cell: a - // hexagonal prism with 12 vertices and 8 faces (two hexagons and six - // quads). VTKIO::read() always uses the *parallel* XML reader, which - // requires a genuine PUnstructuredGrid (.pvtu) file -- a plain serial - // UnstructuredGrid .vtu file is not sufficient, even for one piece. - Mesh mesh(*TestCommWorld); - // Without this, prepare_for_use()'s default renumbering can reassign - // node ids (by local element-traversal order rather than preserving - // the ids we read), which would invalidate the "ids match VTK point - // ordering" assumption the face checks below rely on. - mesh.allow_renumbering(false); - mesh.read("meshes/hex_prism_polyhedron.pvtu"); - mesh.prepare_for_use(); - CPPUNIT_ASSERT_EQUAL(dof_id_type(1), mesh.n_elem()); // The element may live on a single processor when the mesh is @@ -399,39 +386,61 @@ public: CPPUNIT_ASSERT(faces == expected_faces); } - // VTKIO::read() always uses vtkXMLPUnstructuredGridReader, a *parallel* - // XML reader that requires a genuine PUnstructuredGrid (.pvtu) file. - // Handed the underlying serial UnstructuredGrid piece file directly -- - // an easy mistake, since it's a perfectly valid VTK XML file, just not - // the type this reader needs -- it used to fail silently (producing an - // empty mesh with no diagnostic at all) rather than erroring clearly. - // - // We construct VTKIO directly here rather than going through - // mesh.read(), which would dispatch via NameBasedIO's rank-0-reads/ - // then-broadcast pattern: throwing out of that on rank 0 before the - // broadcast would leave every other rank blocked on a broadcast that - // never arrives. Every rank hits this same, rank-independent error - // when calling VTKIO::read() directly, so there's no such hazard here. - void testVTKReadWrongFileType () + void testVTKReadPolyhedra () { LOG_UNIT_TEST; -#ifdef LIBMESH_ENABLE_EXCEPTIONS + // This .pvtu (+ piece) file contains a single VTK_POLYHEDRON cell. + // VTKIO::read() sniffs the file's actual root element to tell a + // parallel PUnstructuredGrid (.pvtu) descriptor from a plain serial + // UnstructuredGrid (.vtu) file and uses the matching reader class -- + // see testVTKReadPolyhedraSerial below for the latter. Mesh mesh(*TestCommWorld); - VTKIO vtk(mesh); - CPPUNIT_ASSERT_THROW_MESSAGE - ("Wrong VTK file type (serial UnstructuredGrid, not " - "PUnstructuredGrid) not detected", - vtk.read("meshes/hex_prism_polyhedron_0.vtu"), - libMesh::LogicError); -#endif + // Without this, prepare_for_use()'s default renumbering can reassign + // node ids (by local element-traversal order rather than preserving + // the ids we read), which would invalidate the "ids match VTK point + // ordering" assumption checkHexPrismPolyhedron() relies on. + mesh.allow_renumbering(false); + mesh.read("meshes/hex_prism_polyhedron.pvtu"); + mesh.prepare_for_use(); + + checkHexPrismPolyhedron(mesh); + } + + // The same polyhedron, read directly from the underlying serial + // UnstructuredGrid piece file rather than its .pvtu descriptor. This + // is a perfectly valid, common thing to hand a mesh reader -- many + // external tools write only this form, with no parallel wrapper -- but + // VTKIO::read() briefly lost the ability to read it at all: a Jan 2024 + // change to add .pvtu support replaced the serial reader class instead + // of adding the parallel one alongside it, silently regressing plain + // .vtu files to fail (which is what caused this CI failure in the + // first place). This is the regression test for that. + void testVTKReadPolyhedraSerial () + { + LOG_UNIT_TEST; + + Mesh mesh(*TestCommWorld); + mesh.allow_renumbering(false); + mesh.read("meshes/hex_prism_polyhedron_0.vtu"); + mesh.prepare_for_use(); + + checkHexPrismPolyhedron(mesh); } // A file that isn't a VTK XML file at all (wrong format entirely, or a - // corrupted/truncated download) should also be rejected clearly. We - // reuse an existing, unrelated mesh fixture here rather than adding a - // new one; any non-VTK-XML file demonstrates this code path. See - // testVTKReadWrongFileType above for why VTKIO is constructed directly. + // corrupted/truncated download) should be rejected clearly rather than + // being handed to vtkXMLFileReadTester/the VTK XML readers and failing + // in some less legible way. We reuse an existing, unrelated mesh + // fixture here rather than adding a new one; any non-VTK-XML file + // demonstrates this code path. + // + // We construct VTKIO directly here rather than going through + // mesh.read(), which would dispatch via NameBasedIO's rank-0-reads/ + // then-broadcast pattern: throwing out of that on rank 0 before the + // broadcast would leave every other rank blocked on a broadcast that + // never arrives. Every rank hits this same, rank-independent error + // when calling VTKIO::read() directly, so there's no such hazard here. void testVTKReadNotAnXMLFile () { LOG_UNIT_TEST;