Skip to content

Gmsh (.msh) ​

The Gmsh mesh format, supporting file versions 2.2, 4.0 and 4.1, in ASCII and binary.

Format namegmsh (writes v4.1), gmsh22 (writes v2.2)
Extensions.msh
Read / Write✓ / ✓
Extra dependencies—

Reading & writing ​

The native reader shares a bounded byte cursor with other formats while retaining ASCII/binary positioning and floating-spelled identifiers (2.0, 1e3); no numeric parse requires a source terminator. Names/arrays remain owned. See the cursor evidence, which measures a supported single-type Gmsh 4.1 ASCII input without changing mixed-cell conformance.

Time/metadata headers also parse source views directly rather than constructing temporary owning rows. The shared stream's existing temporary ownership remains supported and tested; see the source-copy scope and neutral/fluctuating results.

python
import meshioplusplus

mesh = meshioplusplus.read("mesh.msh")          # version auto-detected
meshioplusplus.gmsh.write("out.msh", mesh,
    fmt_version="4.1",  # "2.2", "4.0" (Python writer), or "4.1"
    binary=True,
    float_fmt=".16e",
)
  • fmt_version — output MSH version ("2.2" and "4.1" use the native writer; "4.0" uses the Python reference writer).
  • binary — write the node/element/data bodies in binary (True) or ASCII (False).
  • float_fmt — ASCII coordinate format string.

Via the generic dispatch, file_format="gmsh" writes v4.1 and file_format="gmsh22" writes v2.2.

File structure ​

$MeshFormat is read first: version filetype datasize (filetype 0=ascii, 1=binary; if binary, a 4-byte integer 1 follows to detect endianness). The version string's major component picks the reader: "2"/"2.2" → the 2.2 reader, "4.0" → the 4.0 reader, "4"/"4.1" → the 4.1 reader. $Comments blocks before $MeshFormat are skipped.

Version 2.2: $PhysicalNames; $Nodes (ascii id x y z rows, or binary (int32 id, 3×double) structs); $Elements (ascii: id type ntags tag1..tagN node1..nodeK per line; binary: a per-block header elem_type num_elems num_tags then flat int32 rows of [element_id, tags…, nodes…]). The first two element tags are gmsh:physical/gmsh:geometrical; further tags produce a warning. Optional $Periodic, $NodeData/$ElementData (post-processing views).

Version 4.0 adds $Entities: per-dimension counts, then per-entity tag, 6 doubles (bbox, discarded, even for point entities), num_physicals, physicals[] (plus, for dim>0, a discarded BREP-bounding-entity list). $Nodes and $Elements have two-count headers (numEntityBlocks, total count), then blocks headed by entityTag entityDim type count. Node records interleave an int32 tag with three doubles; element records are int32 tags and node tags. Elements reference nodes by node tag, not by array index, requiring an inverse tag→index remap before connectivity can be built. The native reader accepts ASCII and binary; binary counts are historical producer-width unsigned long values, inferred as 4 or 8 bytes by validating counts and section terminators (the 4.0 header reports sizeof(double), not that width).

Version 4.1 (the default write target) restructures the block headers: $Entities starts with numPoints numCurves numSurfaces numVolumes (4 size_ts); each entity also records its numBoundingXxx BREP-boundary count (populating cell_sets["gmsh:bounding_entities"]). $Nodes header: numEntityBlocks numNodes minNodeTag maxNodeTag; per block entityDim entityTag parametric numNodesInBlock, then a node-tag list, then a matching coordinate list — tags may be sparse or out of order (handled via np.unique(..., return_inverse=True)). $Elements header: numEntityBlocks numElements minElementTag maxElementTag; per block entityDim entityTag elementType numElementsInBlock, then rows of elementTag node1..nodeK.

$NodeData/$ElementData (used across versions): a block of string tags (the first is the data name), real tags (the first is the time value), integer tags [timestep, num_components, num_items], then num_items × (1+num_components) values — the leading column is the 1-based node/element index, discarded after use. See Selecting a time step for how the reader picks between several sections sharing one name.

Cell types & node ordering ​

$Elements type codes map to meshio++ types via a large table (_gmsh_to_meshio_type in common.py) covering linear through very-high-order elements — types 1 through 110, spanning line…line11, triangle…triangle66, quad…quad121, tetra…tetra286, hexahedron…hexahedron1000, wedge…wedge550, and pyramid/pyramid13/pyramid14.

Five element types need a node-order permutation between Gmsh and meshio++ (everything else uses natural order):

typegmsh → meshio++meshio++ → gmsh
tetra10[0,1,2,3,4,5,6,7,9,8] (self-inverse)same
hexahedron20[0,1,2,3,4,5,6,7,8,11,13,9,16,18,19,17,10,12,14,15][0,1,2,3,4,5,6,7,8,11,16,9,17,10,18,19,12,15,13,14]
hexahedron27hex20 permutation + [22,23,21,24,20,25,26]hex20 inverse + [24,22,20,21,23,25,26]
wedge15[0,1,2,3,4,5,6,9,7,12,14,13,8,10,11][0,1,2,3,4,5,6,8,12,7,13,14,9,11,10]
pyramid13[0,1,2,3,4,5,8,10,6,7,9,11,12][0,1,2,3,4,5,8,9,6,10,7,11,12]

Data mapping ​

  • cell_data["gmsh:physical"], cell_data["gmsh:geometrical"] — the first two element tags (also recognized as "cell_tags" on write).
  • point_data["gmsh:dim_tags"] — v4.1 only, an (N, 2) int array of (entity_dim, entity_tag) per node.
  • cell_sets["gmsh:bounding_entities"] — v4.1 only. Signed entity tags (the sign is the boundary's orientation), so these are not cell indices and cannot be a region; they take the cell_sets verbatim passthrough. In C++ they ride the GmshInfo side channel (see below).
  • field_data[name] = [phys_num, phys_dim] — from $PhysicalNames.
  • Arbitrary point_data/cell_data from $NodeData/$ElementData.
  • mesh.gmsh_periodic — a mesh-level attribute (not a data-dict key) holding [dim, (slave_tag, master_tag), affine_or_None, node_pairs] per periodic relation, from $Periodic. Entity tags remain file ids; node_pairs is an owning Int64 (N, 2) array of 0-based mesh point rows, resolved from file node tags even when sparse or out of order. None and an empty affine array both mean no transform. match_periodic_nodes computes the node pairs of such a record from two named regions and a transform.

Named regions ​

A Cell region (mesh.regions) with its own gmsh tag (mTag >= 0, from a prior gmsh read) writes exactly that tag. A region with no tag — every Cell region Abaqus (*ELSET), MED (a family) or MDPA (a SubModelPart) produce, none of which has a gmsh-style integer physical id — gets a freshly allocated one instead of being dropped (v11.5.0, tier B3):

python
mesh = meshioplusplus.abaqus.read("part.inp")  # an ELSET -> mesh.regions, mTag == -1
meshioplusplus.write("part.msh", mesh, file_format="gmsh22")
back = meshioplusplus.read("part.msh")
back.regions  # the same region, now with a real gmsh tag
  • Allocation is max(existing tags of that dimension) + 1, one counter per dimension (so a surface group and a volume group can both start small — gmsh disambiguates by the (dim, tag) pair, never tag alone), counting upward deterministically in region order. A region's dimension is its own mDim if set, else inferred from its first member cell's type — true of every Abaqus/MED/MDPA-origin region, none of which set mDim either.
  • Two untagged regions sharing a cell: gmsh allows only one physical tag per element, so the first (region order) keeps the cell and a warning names both.
  • Format 4.1 records physical-group membership only through $Entities, one entity per cell block (this writer's $Elements model). A region that covers a whole block round-trips fully — $Entities is synthesized, one entity per cell block, its nodes the highest-dimension block touching each one (gmsh's own convention: a lower-dimension region's nodes inside a volume belong to the volume). An entity is a (dim, tag) pair, so two blocks of one dimension (a triangle and a quad block, or a tetra and a hexahedron block, even of one region) need two tags: a block keeps its region tag as its entity tag while that tag is positive and no earlier block of the dimension holds it, and every other block takes the smallest free positive tag of its dimension, in block order. A region that covers only part of a block cannot be represented that way in 4.1 today: that block gets no physical group (tag 0), with a warning naming it — the name still survives via $PhysicalNames, only membership for that block is lost. gmsh22's per-element tag column has no such restriction and always round-trips membership exactly; prefer it for a region that does not align with cell-type blocks.
  • Both engines. The pure-Python writer (gmsh/main.py, _with_region_groups) applies the same rule as the C++ one: a Cell region's own tag is kept, any other gets a fresh one per dimension in region order, and the groups reach $PhysicalNames and the per-element physical tag. Cell regions already described by field_data are not duplicated.

Selecting a time step ​

A transient Gmsh file carries several $NodeData/$ElementData sections under one name, each with its own real tag (time value). Since v11.3.0 (tier B1) ReadOptions::mTimeStep (0-based, negative counts from the end — the ResolveTimeStep contract, see selective reads) resolves against the sorted, deduplicated union of every section's time value across the whole file, and the reader keeps only the sections whose time matches the resolved one — for every name, not just one.

With the default step (0), behaviour is unchanged from every prior release: no pre-scan runs at all, and the legacy rule applies verbatim — the first section per name, in file order, wins. Only a non-default mTimeStep triggers the (cheap, second-pass, header-only) time scan.

read_gmsh_metadata (4.1 only, native — 2.2 has no header-only metadata path at all and falls back to a full read regardless of time) reports the same union in time_values; a name pushed once per step is deduplicated in the summary, matching the single-array-per-name a real read always returns.

Quirks & limitations ​

  • Section keywords may be indented (FEconv's samples indent every line); leading and trailing whitespace around $Nodes, $Elements and the other markers is ignored. Before v16.6.0 both engines refused such a file.
  • Version strings are normalized: "2" → 2.2, "4" → 4.1.
  • Gmsh can't distinguish a (n,) shape from (n,1) for post-processing data; the reader squeezes single-component arrays to 1D.
  • Elements in v4.0/4.1 are addressed by node tag, not array position — the most structurally distinctive quirk of this format relative to nearly every other one meshio++ supports.
  • 4.0 files read through the same C++ core from Python, C, Fortran, Julia, R, WASM and the native CLI; periodic files require the metadata channel described below (the native CLI has no such channel and refuses them). Like the Python reference, the 4.0 path does not synthesize gmsh:dim_tags or retain bounding-entity lists; physical tags and named cell regions are preserved. Parametric nodes and byte-swapped 4.0 binary files are refused. Native metadata for 4.0 falls back to a full read; the cheap header-only path remains 4.1-only.
  • v4.1 physical-group membership lives in $Entities, not on the elements. A 4.1 $Elements block names an (entityDim, entityTag) pair and the physical tag hangs off the entity, so a file whose $Entities is missing or unread has geometry but no gmsh:physical and no named groups — unlike 2.2, where each element carries its own tag. Writing 4.1 therefore needs point_data["gmsh:dim_tags"] (which says what entity each node belongs to) for membership to survive; a mesh that already has it (a genuine 4.1 round trip) writes it unchanged. A mesh that instead carries Cell regions but no gmsh:dim_tags gets $Entities synthesized from those regions (v11.5.0, tier B3) when they align with whole cell blocks — see Named regions above; gmsh22 remains the format to prefer for a region that does not. A mixed mesh (more than one cell block) with no regions gets the same synthesis, one entity per block with tags unique within each dimension and no physical group invented, so it writes without gmsh:dim_tags; a single-block mesh keeps the one-node-block output with no $Entities, and a mesh that already carries gmsh:geometrical keeps its own entity tags. As in any 4.1 file with entities, $Nodes is grouped by entity, so the points read back in entity order rather than the order they were written in (the cells' coordinates are unchanged). Engine difference: only the C++ writer synthesizes entities; the pure-Python 4.1 writer (gmsh/_gmsh41.py) still raises WriteError for a mixed mesh without gmsh:dim_tags, and it is reached only when the C++ core declines or is not built.
  • A 4.0/4.1 file that tags only some entities yields gmsh:physical for every cell block, 0 (gmsh's "no physical group") on the untagged ones. The pure-Python reference instead omits the untagged blocks, which leaves gmsh:physical shorter than mesh.cells; the C++ core cannot represent that (one array per block is a uniform-API invariant) and does not reproduce it. A file that tags nothing gets no gmsh:physical key at all on either path.
  • $Periodic record layout differs across all three versions (e.g. v4.0 binary uses a negative node count as a sentinel meaning "an affine transform follows", then reads a fixed 16 floats for that transform).
  • The C++ type table covers up through hexahedron125/tetra286(sic — the exact upper bound is a curated subset, not the full ~110-entry Python table); a file referencing a higher-order type outside that subset falls back to Python transparently.
  • The shim tries the C++ reader first for all three versions, including $Periodic, using the shared fallback policy for unsupported constructs; non-default time_step errors propagate instead of falling back. On write, C++ is attempted for float_fmt == ".16e" and fmt_version of "2.2" or "4.1", with or without periodic links. v4.0 writes and non-default float formats still use Python. (Before v9.7.0 a v4.1 write carrying gmsh:dim_tags did too, because the C++ writer could not emit $Entities.)

Periodic metadata across bindings ​

Native reads keep $Periodic for 2.2, 4.0 and 4.1, in ASCII and binary; native writes emit it in 2.2 and 4.1. The 2.2 section stays textual even in binary files; 4.0 binary uses producer-long counts and Int32 node tags, with -1 followed by 16 doubles for an affine transform; 4.1 uses size-width counts/node tags. The reader validates counts, entity dimension 0–3, positive Int32 entity tags, finite affine transforms of 0 or 16 coefficients, positive node tags and references present in $Nodes, and an exact $EndPeriodic. Links, node pairs and duplicate pairs retain file order; the section may precede $Nodes. Empty links survive. Writers validate the same metadata and (N, 2) Int64 point indices before opening the output, then translate point rows into their sequential node tags. Entity ids stay unchanged; entity existence and geometric consistency are not inferred or repaired.

Use a metadata-bearing path: Python's native binding automatically carries mesh.gmsh_periodic; C++ uses read_gmsh(path, GmshInfo&, opts) and write_gmsh22 / write_gmsh41(path, mesh, binary, info). A successful read replaces GmshInfo; a failed read leaves it untouched. GmshInfo::mPeriodic holds GmshPeriodicLink records (mEntityTags, mAffine, mNodePairs); mBoundingEntities still holds 4.1 signed boundary tags per cell block. C / Fortran / Julia / R use their read_with_info / write_with_info APIs; WASM uses readMeshSelective(path, {format: 'gmsh', info: true}), with mesh.info.periodic and automatic restoration on writes to gmsh or gmsh22. See the language pages for shape and indexing conventions.

The info-less native reader and registry refuse any $Periodic section, including with lenient, rather than silently discard it. This also applies to the native CLI and info-less flat-binding calls. Metadata summaries may read periodic files through the full info-bearing reader, marked as a full-read fallback. C++ ABI 22 is required (GmshInfo grew); existing C ABI layouts are unchanged.

Mesh operations do not remap periodic format metadata. The channel preserves I/O, not topology edits: after removing/reordering points, discard or explicitly rebuild the records (for example with match_periodic_nodes) before writing. Out-of-range rows are rejected, but an in-range stale row cannot be detected automatically.

Notes ​

  • tests/python/meshes/msh/insulated-2.2.msh and insulated-4.1.msh — the same mesh (Gmsh 4.2.2, -format msh2 for the 2.2 variant): 111 triangles (2 Physical Surfaces) + 21 lines (1 Physical Line). Used to check point sums, cell counts, and gmsh:physical/gmsh:geometrical/cell_sets consistency, including the v4.1 $Entities-bearing variant.

Released under the MIT License.