MFEM mesh and grid functions (.mesh, .gf)
MFEM's own ASCII mesh, the file its examples write and GLVis opens, and the grid functions (.gf) that hold a field on it. A mesh lists elements and boundary elements by an integer attribute and a geometry code, then either vertices with their coordinates or, for a curved high-order mesh, a nodes grid function that gives the geometry.
| Format name | mfem |
| Extensions | .mesh, shared with Medit: a file whose first line names an MFEM mesh is read as one |
| Read / Write | ✓ / ✓ (grid functions both ways) |
| Extra dependencies | — |
Reading & writing
The native lexer shares bounded source-line and token-record positioning while preserving comment, header, quoting and escape behavior; normalized token text remains deliberately owned. Its temporary source strings do not escape into returned arrays/names. The cursor comparison is a consolidation result with unchanged allocation evidence, not a tokenizer speedup claim.
import meshioplusplus
mesh = meshioplusplus.read("star-q2.mesh") # an order-2 mesh
mesh = meshioplusplus.mfem.read("refined.mesh", {"u": "sol.gf"}) # with a field
mesh = meshioplusplus.mfem.read("refined.mesh", ["sol.gf"]) # named by its stem
mesh = meshioplusplus.read("fichera-q3.mesh") # order 3: VTK Lagrange cells
mesh = meshioplusplus.mfem.read("run.mesh.000000", {"u": "u.000000"}) # a parallel run, merged
meshioplusplus.mfem.write("out.mesh", mesh, grid_functions=True) # + out.<name>.gf
nurbs = meshioplusplus.read("pipe-nurbs.mesh") # a NURBS mesh...
nurbs.info["nurbs"]["patches"][0]["knots"] # ...keeps its patches
meshioplusplus.mfem.write("pipe.mesh", nurbs) # MFEM NURBS mesh v1.0 againBoth engines (the C++ core and the pure-Python reference) read the same meshes and write the same bytes. In C++ the fields are read_mfem(path, u) and write_mfem(path, mesh, true). The MCP server's convert takes the same grid_functions and write_grid_functions options.
.mesh is shared with Medit. Since v16.20.0 the ambiguous-extension read loop tries first the candidate sniff_format recognises from the file's first bytes, so an MFEM file goes to mfem directly rather than through a Medit decline; every candidate is still tried if that guess is wrong. The native resolver (CLI, C, Fortran, Julia, R, WASM) looks at the first line of an existing .mesh file and hands an MFEM mesh … file to mfem the same way. .mesh is not the only content-aware extension default: Marc's .dat (v16.8.0) is read the same way, by content rather than by name. Writing a .mesh without a format still writes Medit, since a write resolves by name alone (resolve_write_format, v16.17.0): pass file_format="mfem" (CLI --output-format mfem).
Reading
| Version | Read as |
|---|---|
MFEM mesh v1.0 | a conforming mesh |
MFEM mesh v1.2 | the same, up to mfem_mesh_end; the rank file of a parallel mesh is read with its siblings (see Parallel meshes) |
MFEM mesh v1.3 | v1.2 plus attribute_sets and bdr_attribute_sets |
MFEM mesh v1.1 | a conforming mesh; one with the legacy vertex_parents and coarse_elements sections is its leaf mesh, which is what it lists |
MFEM NC mesh v1.0, v1.1 | its leaf elements (see Non-conforming meshes) |
MFEM NURBS mesh v1.0, v1.1 | its knot-span elements, sampled from the patches (see NURBS meshes) |
MFEM NURBS NC-patch mesh, INLINE | refused with a ReadError naming why |
- Elements and boundary elements are separate cell blocks, one per type, the elements first. Geometries 0–7 map to
vertex,line,triangle,quad,tetra,hexahedron,wedgeandpyramid. Every geometry, the prism included, is in meshio++'s node order. - The attribute is the
mfem:attributecell data and anattribute_<n>(element) orboundary_<n>(boundary)cellregion taggedn. A v1.3 attribute set is acellregion of its own name, holding the cells of all its attributes. #lines are comments. Boundary elements stay cells, like Elmer's, because MFEM allows boundary elements between two elements and interior ones, which a side region cannot express.
Curved meshes
The nodes grid function names its space by a finite element collection:
| Collection | Read as |
|---|---|
H1_<d>D_P1, Linear, H1Pos_…_P1 | linear cells, vertices from the nodes |
H1_<d>D_P2, H1@…_P2, Quadratic | order-2 cells: line3, triangle6, quad9, tetra10, wedge18, hexahedron27 |
H1_<d>D_P<p>, H1@G/H1@U…_P<p> with p ≥ 3, and the legacy Cubic (v16.11.0) | VTK Lagrange cells of order p (VTK_LAGRANGE_CURVE, …_TRIANGLE, …_QUADRILATERAL, …_TETRAHEDRON, …_HEXAHEDRON, …_WEDGE), their nodes on the equispaced lattice VTK uses |
L2_T1_<d>D_P1 (periodic meshes) | linear cells, each element with its own points |
Bernstein H1Pos_<d>D_P<p> at order 2+ (v16.12.0) | curved cells of order p, like H1 (see below) |
serendipity H1Ser_2D_P<p> at order 2+ on a quadrilateral mesh (v16.12.0) | curved cells of order p, like H1 (see below) |
| serendipity on a mesh with other cells, and other bases | the vertices only, with a warning |
| anything else | a ReadError |
An order-2 mesh's points are MFEM's degrees of freedom, in MFEM's order: the vertices, then the edges in order of first appearance (elements in file order, each element's edges in MFEM's local order), then the quadrilateral faces in 3-D (numbered over all faces the same way), then the element interiors. Each cell's nodes are placed by the vertices of the edge, face or cell they sit on, so edge and face orientation never matters at order 2. A mesh with pyramids is read at order 1, since meshio++ has no curved pyramid.
Arbitrary order (v16.11.0). MFEM's order-p nodes sit on Gauss–Lobatto points (H1@G, the default), closed-uniform points (H1@U) or, for Cubic, the equispaced points of MFEM's legacy cubic elements. meshio++ numbers MFEM's degrees of freedom the way MFEM does (vertices, edges, faces, interiors; each edge and face in the orientation of the element that first holds it), places every one at its reference position, and evaluates each cell's nodal basis at the VTK Lagrange points by interpolation, so the curved geometry is kept exactly (an order-p polynomial through order-p points) rather than resampled. Nodes shared by neighbouring cells are shared points. The result is what MFEM's own high-order VTK output (ParaViewDataCollection with SetHighOrderOutput) holds, point for point. meshio++ has no fixed type for these cells: they are the ragged VTK_LAGRANGE_* types, which VTU and VTK write and read.
Bernstein and serendipity spaces (v16.12.0). Their degrees of freedom are coefficients, not values at points: a Bernstein (H1Pos) coefficient sits on the uniform lattice and weighs the Bernstein polynomial of its multi-index (tensor products on quadrilaterals and hexahedra, barycentric on triangles and tetrahedra, triangle times segment on prisms); MFEM's serendipity quadrilateral (H1Ser, 2-D only) has nodal Gauss–Lobatto edge functions, vertex functions corrected by them and, from order 4, Legendre bubbles with no position. meshio++ numbers them like H1 (with the serendipity interior count) and evaluates MFEM's basis at every VTK Lagrange node, at order 2 as well, so the cells are what MFEM's element transformation gives. Grid functions in these spaces are read the same way; a serendipity one on a mesh of other cells is skipped with a warning (MFEM's serendipity collection makes its triangles Bernstein).
NURBS meshes
An MFEM NURBS mesh (v16.12.0) is a topology of patches (segments, quadrilaterals or hexahedra) and their boundary, the knot vector of every edge, and the control points with their weights: either once for the whole mesh, in MFEM's global numbering (a NURBS grid function after the weights), or per patch (patches, homogeneous x w, y w, w or controlpoints_cartesian). meshio++ numbers everything the way MFEM's NURBSExtension does (vertices, then the interior control points of every edge, face and patch, each edge and face in its orientation), so it builds the same knot-span elements MFEM does, in MFEM's order, with MFEM's vertex numbers. Each becomes one cell of the highest knot-vector order in the mesh: line/quad/hexahedron at order 1, line3/quad9/hexahedron27 at order 2, VTK Lagrange cells above. Its nodes are the rational patch geometry evaluated at the cell's lattice points, and nodes shared by neighbouring cells, across patches too, are one point. The boundary patches become boundary cells the same way; without a boundary section, the patch faces no other patch shares are the boundary, attribute 1, as MFEM builds them. v1.1's spacing formulas only matter for refinement and are skipped.
A NURBS curve or surface is rational and a VTK Lagrange cell is polynomial, so between the lattice points the cells approximate the patches (at the nodes they are exact). A grid function on the mesh's own NURBS space (NURBS<p> or NURBS, in either header form MFEM writes) is evaluated the same way at every node; an L2 order-0 one is cell data. Refused: mesh_elements (a subset of the knot spans), periodic meshes, files without an edges section (MFEM would derive them), and the non-conforming NC-patch variant.
The patches themselves: the NURBS side channel
The sampling keeps the geometry at the nodes but not the patches, so since v16.40.0 a read also returns what it drops, as a NurbsInfo side channel (C++ nurbs.hpp, the GmshInfo pattern): read_mfem(path, grid_functions, options, info) in C++, and mesh.info["nurbs"] in Python, where both engines fill the same dict, array for array. Per patch it holds the degree and knot vector of every parametric direction (oriented along the patch's own directions, as MFEM orients them), the control net (Cartesian, direction 0 fastest) and its weights, and the control values of every NURBS grid function read with it (field_names names them); then the file's patch topology (patch and boundary-patch corners with their attributes, the edges section and the vertex count); and the cell map: the patch and knot span of every element cell (the first block) and the boundary patch of every boundary cell. Every index is 0-based in C++ and Python. A mesh that is not NURBS has none, and the shared registry (the native CLI, a plain mio_read) drops it.
The other surfaces carry it explicitly: the C API's mio_read_with_info/mio_write_with_info with the mio_nurbs_info_* accessors (C API), Fortran's info%nurbs_* (Fortran), Julia's nurbs_info (Julia), R's mio_nurbs_info (R) and WASM's readMeshSelective(path, {info: true}) (WASM).
The patches are not only kept: since v16.43.0 the isogeometric layer acts on them. mp.iga_refine(mesh, subdivisions=2, elevate=1) inserts knots and elevates degrees exactly (geometry and NURBS fields unchanged, every direction alike because patches share knot vectors), mp.iga_evaluate evaluates a patch with derivatives, and a refined mesh writes back as an MFEM NURBS mesh.
Non-conforming meshes
An MFEM NC mesh (v16.11.0) stores a refinement tree: root elements (rank attr geom ref_type and their children or vertices, geom = -1 for an unused slot), vertex_parents (a vertex made on the edge between two others; v1.1 adds the position along it) and the coordinates of the root vertices. meshio++ reads the leaf elements and boundary elements, placing each new vertex from its parents; the hanging nodes are left as they are (a leaf's edge may end in the middle of its neighbour's edge), with a warning. Elements of other ranks (a parallel NC file) are dropped with a warning. Since v16.12.0 the leaves come in MFEM's own order, along its space-filling curve (each root from its root_state, the children of a quadrilateral refined in both directions or a hexahedron refined in all three in Hilbert order, others in child order), and the points in MFEM's vertex order (the top-level vertices by node id, then the others as the leaves meet them), so grid functions saved on the mesh apply: H1 fields of any order (their edges and faces numbered over the leaves as on a conforming mesh) and element-wise ones. The values at hanging nodes are the ones MFEM stored, so a field reads continuous where MFEM made it so.
Parallel meshes
A parallel MFEM run saves one file per rank, <prefix>.000000, <prefix>.000001, … (v16.11.0). Opening any one of them reads the whole mesh: its siblings are found beside it, and shared vertices and higher-order nodes are merged, so every cell appears once, and cell_data["partition:part"] holds each cell's rank.
ParMesh::ParPrintfiles (MFEM mesh v1.2, withcommunication_groupsand the shared vertices, edges and faces of each group) are merged through their groups.ParMesh::Savefiles are plain serial meshes whose boundary also lists the faces shared with other ranks. Their boundary vertices are merged by position (to1e-9of the mesh size), and a boundary face that two ranks both list, being interior, is dropped.piece=k(mfem.read(path, piece=k),ReadOptions::mPiecein C++) reads rank k alone.- A rank's grid function is named by one of its files:
{"u": "sol.000000"}readssol.000000,sol.000001, … with the mesh.
Non-conforming parallel meshes (v16.12.0). ParMesh::ParPrint of a non-conforming mesh writes each rank's refinement tree as an MFEM NC mesh with a rank and the neighbouring ranks' leaves as ghosts. Each rank is read as its own leaves in MFEM's local order and numbering (see Non-conforming meshes), so its grid function applies; the vertices its ghosts share are merged by position, and only the boundary faces of its own leaves are kept. ParMesh::Save writes such a mesh as plain rank meshes with hanging nodes, merged like the conforming ones, except that where the interface between two ranks is itself non-conforming, their interface faces do not match each other and stay as boundary cells.
Grid functions
| Space | Read as |
|---|---|
H1 order 1 or 2 | point_data (vectors as (n, vdim)); both Ordering: 0 (byNODES) and 1 (byVDIM) |
H1 order 3 and up (v16.11.0) | point_data at the VTK Lagrange nodes, interpolated as the geometry is |
L2 order 0 | cell_data, element cells only; NaN on the boundary cells |
| anything else | skipped, with a warning |
An order-2 field on a linear mesh makes the cells quadratic, with the new nodes at the edge, face and cell centres, and an order-p field on a lower-order mesh makes them VTK Lagrange cells of order p. An order-1 field on an order-2 mesh is interpolated linearly to the extra nodes, which is exact. A field whose size does not match the mesh is a ReadError.
A field must be saved with the mesh it lives on. MFEM re-marks triangles and tetrahedra when it loads a mesh (it reorders their vertices for refinement), and a grid function numbers its degrees of freedom on the re-marked mesh. A .gf saved in a session therefore matches the mesh printed in that session (mesh.Print), not always the original file of a triangle or tetrahedral mesh; pair them as MFEM's own examples do.
Writing
- The cells of the highest dimension are the elements, and those one dimension lower the boundary. Each facet of a
sideregion becomes one more boundary element. Lower-dimensional cells are dropped with a warning. - The attribute of each cell is
mfem:attribute, else its cell region's tag (or a fresh number), else 1. A cell region other thanattribute_<n>/boundary_<n>becomes an attribute set, which makes the fileMFEM mesh v1.3; otherwise it is v1.0. - Quadratic cells make an
H1_<d>D_P2nodesspace.quad8,hexahedron20andwedge15are completed with their face and body centres from the serendipity map, which keeps them curved. Linear cells in a quadratic mesh get their extra nodes at the centres. - VTK Lagrange cells of order p (v16.11.0) make an
H1_<d>D_P<p>space on Gauss–Lobatto points, their nodal values interpolated from the VTK lattice; MFEM reads such a file back to the same geometry. Lower-order cells in it are raised to order p exactly; serendipity cells are placed from their corners, with a warning. A mesh with pyramids is written with corners only, with a warning. - Point coordinates and field values are written with 17 significant digits.
grid_functions=Truewrites one<stem>.<name>.gfper data array: point data asH1at the mesh's order and cell data asL2order 0. Without it, and for field data always, data is dropped with a warning and a provenance note. Point regions are dropped the same way (MFEM has no node sets).- A NURBS mesh is written back as one (v16.40.0). A mesh read from an
MFEM NURBS meshcarries its patches (above), andwritewrites them as anMFEM NURBS mesh v1.0in MFEM's global form (knotvectors,weightsand aNURBS<p>nodes space with Cartesian control points,NURBSwhen the degrees differ), so reading the file gives back the mesh and the side channel exactly, whichever form the source used; both engines write the same bytes. The patches, their attributes and the boundary come from the side channel. The mesh must still be their sampling (its points, its element cells and the point data of the NURBS fields, within1e-9of their size): a mesh moved, smoothed or re-meshed since is written as its sampled cells instead, with a warning (nurbs=None, the default);nurbs=Truerefuses it (WriteError, "the mesh no longer matches its NURBS side channel") andnurbs=Falsealways writes the cells. Withgrid_functions=Trueeach NURBS field whose point data the mesh still has is written as aNURBS<p>grid function and element cell data asL2order 0; other point data, field data and regions other than the attributes are dropped with a warning. v1.1 spacing formulas are not written (the knots are). In C++ the overload iswrite_mfem(path, mesh, grid_functions, info); the registry and native CLI write the sampled cells.
Verification
Reading and writing were checked against MFEM 4.10 itself through PyMFEM, with MFEM's own sample meshes (BSD-3, the test fixtures):
- For every element of 14 samples (orders 1–3, triangles to pyramids, curved and mixed), every node meshio++ reads sits where MFEM's element transformation puts it, to
0.0. - MFEM reads the files meshio++ writes to the same geometry and field values, to
1.7e-16. - Arbitrary order (v16.11.0): on 14 meshes of orders 3–5 (MFEM's
fichera-q3,escher-p3,toroid-wedgeandrt-2d-p4-tri, and one-element reference meshes of every shape curved and warped by MFEM, Gauss–Lobatto and closed-uniform), every Lagrange node and field value equals MFEM's own high-order VTK output to1.1e-13, and MFEM reads the order-p files meshio++ writes to1.4e-13. - Non-conforming (v16.11.0): the leaf and boundary elements of
amr-quadmatch the ones MFEM builds, attribute and corners. v16.12.0: on MFEM'samr-quadandamr-hexand on quadrilateral, hexahedral, triangular and tetrahedral meshes MFEM refined non-conformingly itself, every leaf has MFEM's index and vertex numbers, andH1fields of orders 1–3 and anL2one equal MFEM's values at every node to5e-15;reference_nc.npzfreezes the first two. - Parallel (v16.11.0): meshes MFEM saved over 3 and 4 ranks with MPI, in both layouts, read to MFEM's own per-rank high-order output, with exactly the serial mesh's vertex count once merged.
- Bernstein and serendipity (v16.12.0): meshes of every shape curved into
H1Posspaces of orders 2–4 and quadrilateral meshes inH1Serspaces of orders 2–5, with fields up to order 5, give every VTK Lagrange node and value of MFEM's element transformation andGetValueto3e-15. Eight of them are fixtures (modal/, withreference_modal.npz). - NURBS (v16.12.0): all 19 conforming NURBS meshes in MFEM's
data/(1-D to 3-D, orders 1–4, both control point forms, v1.1) and MFEM's own re-prints of them give MFEM's knot-span elements and boundary elements, vertex for vertex, and every node and value of a projected NURBS field equals MFEM's element transformation andGetValueto3e-14. Six of them are fixtures, withnurbs/reference_nurbs.npz. - NURBS writing (v16.40.0): each of the six NURBS fixtures, written back and read again, gives the same mesh, field and side channel bit for bit, in both engines, which also write the same bytes. MFEM's own reading of the written files has not been run yet (PyMFEM was not available); the files follow the global form MFEM's
Mesh::Printwrites for the same meshes. tools/gen_mfem_fixtures.pyandtools/gen_mfem_parallel_fixtures.pyfreeze MFEM's evaluation, so the tests check every node and field value without MFEM.
Notes
- MFEM's own VTK export reverses prisms. Its
PrismMapexists because classic VTK winds the wedge the other way round from MFEM and meshio++. A prism read from a.meshand one from an MFEM-written.vtktherefore differ in node order, not in shape.