Skip to content

Named regions ​

A region is a named group of mesh entities. Every format that models one spells it differently — gmsh calls it a physical group, Exodus an element block or node set or side set, Abaqus an *NSET / *ELSET / *SURFACE, MED a family or group, UNV a group, Ansys a component, OpenFOAM a boundary patch, LS-DYNA a *PART or *SET_*, Kratos a SubModelPart — and converting between any two of them used to lose or mangle the grouping. Region is the one model they all map onto.

Before meshio++ 8.1 these lived only on the Python Mesh, as point_sets / cell_sets. They never crossed the pybind11 boundary, so the C API, Fortran, WebAssembly and the native CLI dropped them entirely, and each set-capable format smuggled them through its own side-channel struct. Regions close that gap.

The model ​

Every format's group concept maps onto one Region of kind point, cell or side, whose cell entries are global block-major indices

python
import meshioplusplus as mio

region = mio.Region("wall", "side", [[0, 1], [1, 3]], dim=2, tag=7)
FieldMeaning
namethe group's name, as the originating format spelled it
kind"point", "cell" or "side" — what entries indexes
dimthe topological dimension the group was declared for, or -1 if the format does not say
tagthe format-native integer id (gmsh physical tag, MED family id, Exodus set id), or -1
entriesint64 indices — see below

dim and tag exist because gmsh needs them: a physical group is per-dimension, two groups of different dimensions may share a name, and the integer tag is what a round-trip back to gmsh must restore. They are carried through conversion even by formats that have no use for them.

The three kinds ​

"point" — entries are point indices, shape (n,).

"cell" — entries are global cell indices, shape (n,), numbered block-major: block 0's cells first, then block 1's, and so on. This is the same numbering partition_labels uses for its flat buffer. detail/cell_index.hpp is the single owner of the conversion to and from (block, row), so nothing re-derives it — the browser viewer's _dimension_rows counter bug (a running index that failed to advance over skipped blocks, so every per-cell quantity silently shifted) is the cautionary tale for what a second copy of that arithmetic costs.

"side" — entries are (global cell index, local facet index) pairs, shape (n, 2). Facets are numbered exactly as detail/cell_faces.hpp numbers the faces of a 3-D cell and detail/cell_edges.hpp the edges of a 2-D one. This kind has no equivalent in the legacy point_sets/cell_sets model at all — it is only reachable through .regions.

Canonical ordering ​

Entries are stored ascending and de-duplicated (lexicographically on the pair, for "side"). Two regions describing the same group are therefore bitwise equal, which is what makes the cross-format round-trip matrix an equality assertion rather than a set-comparison heuristic, and what makes region output byte-identical across the MESHIO / NATIVE / KRATOS backends and across thread counts.

The visible consequence: a group that survives an operation comes back sorted, not in whatever order the operation's permutation produced. Set semantics are what a named group means.

Compatibility with point_sets / cell_sets ​

mesh.point_sets and mesh.cell_sets keep working exactly as they always have. They are now views over mesh.regions: reading one materializes the historical shape (a flat index array for point_sets, a per-block list of local index arrays for cell_sets), and writing one creates or replaces the matching regions.

python
mesh.point_sets = {"fixed": [0, 3]}     # -> a "point" region
mesh.cell_sets["wall"] = [[0], [1, 2]]  # -> a "cell" region, global indices
mesh.regions                            # both, plus any side regions

The views are dict subclasses, not bare mappings, because plenty of code — including numpy.testing.assert_equal — branches on isinstance(x, dict).

Two accommodations make the compat lossless rather than nearly so:

  • The passthrough escape hatch. Formats stash things in cell_sets that are not cell indices at all: gmsh's gmsh:bounding_entities holds entity tags, orientation sign included. Such an entry cannot be a region, so it is kept verbatim in a private passthrough dict and returned unchanged. The test is non-negative integers, not "integers in range" — a negative value is the reliable tell (in the bundled example.msh, 475 of the 476 populated blocks contain one), while requiring every index to be in range would reject a genuine but sloppily-built cell set, which meshio has always accepted.
  • None blocks become empty arrays. A cell_sets value may carry None for a block with no members (gmsh's reader builds [None] * n). Global indices cannot express "absent" separately from "empty". Every consumer in the tree already treated the two identically — and the Abaqus writer's len(v[ic]) would in fact have raised on a genuine None.

One sharp edge, shared with the old model: indices are captured against the mesh's blocks at the time you set them. Appending a cell block is safe; inserting, removing or reordering one invalidates them.

Recovering global cell indices from cell_sets ​

A consumer that wants "which absolute cells are in this group" — the question a GUI importer usually has, since it numbers elements globally, not per block — should read mesh.regions rather than mesh.cell_sets: the entries are already global, and no format-specific offset arithmetic is needed.

python
for r in mesh.regions:
    if r.kind == "cell" and r.name == "wall":
        global_ids = r.entries  # 0-based, block-major, ready to use

If code must work from mesh.cell_sets instead (its per-block local form), add each block's own offset — the running total of every earlier block's cell count, the same prefix sum block_bases computes internally:

python
offset = 0
global_ids = []
for block, local in zip(mesh.cells, mesh.cell_sets["wall"]):
    global_ids.extend(offset + i for i in local)
    offset += len(block)

This is the fix for a consumer ported from a library or format convention where cell indices were global rather than block-local — see doc/formats/flac3d.md for a concrete case (issue #76).

The format matrix ​

FormatpointcellsidetagNotes
abaqus✅✅✅❌*NSET / *ELSET / *SURFACE. Abaqus names its groups but has no integer id for them.
lsdyna✅✅✅❌*SET_NODE / *SET_SOLID, _SHELL, _BEAM, _PART / *SET_SEGMENT map onto the three kinds, and a *PART is a cell region whose tag is the pid and whose dim is its highest element dimension. On write, a cell region with a dimension becomes a *PART and every other region a set; a set id comes from a positive tag, else it is numbered, so tag is only carried for parts. A segment is matched back to a (cell, facet) by its corner nodes. See doc/formats/lsdyna.md.
elmer❌✅↪️✅A body (bulk elements) and a boundary (boundary elements) are cell regions tagged with their Elmer id and named from mesh.names (else body_<id>/boundary_<id>). On write, a cell region over bulk cells is a body and one over lower-dimensional cells a boundary, keeping a positive unused tag as the id. ↪️ A side region's facets are written as boundary elements, so it reads back as a cell region over a new facet block. No node set, so point regions are dropped. See doc/formats/elmer.md.
febio✅✅✅❌<NodeSet> → point, <ElementSet> → cell, each <Elements> block a cell region tagged with its domain's material id, and a <Surface> whose facets are all faces of solids a side region (matched by corner nodes; other surfaces become their own cell block). On write, a cell region covering exactly one block names that <Elements> block, other cell regions are <ElementSet>s, side regions <Surface>s and point regions <NodeSet>s; material ids are renumbered, so the tag is lost. See doc/formats/febio.md.
xplt📖📖📖✅Read only: FEBio plot-file domains → cell (tagged with the part id), node sets → point, element sets → cell, surfaces → side as for .feb; each data surface is also a facet block with a cell region of its name (v16.10.0). See doc/formats/xplt.md.
xdmf✅✅✅✅<Set> elements (v16.27.0): SetType Node → point, Cell → cell, Face/Edge → side. See XDMF sets.
vtu / vtp✅✅✅✅VTK has no set concept, so meshio++ writes each region as a <FieldData> array (v16.27.0). See The VTK XML convention. A .vtp holds no volume cells, so its side regions are polygon edges.
ansys (Fluent)❌✅¹❌✅Each Fluent zone, a cell zone or a boundary face zone, is a cell region named from its (39 ...)/(45 ...) declaration (zone_<id> otherwise), tagged with the zone id, over the cells (or boundary facet cells) cell_data["ansys:zone"] puts in it. On write (face-based since v16.10.0), the zones come from ansys:zone (else one per block) and a zone takes the name of the region with its tag and dimension, else of the region with exactly its cells. ¹ Only regions shaped like zones name anything: other cell regions, point regions and side regions are not written. See doc/formats/ansys.md.
ansysInp✅✅❌❌A CMBLOCK component is a named list of NODE or ELEM numbers: point and cell regions, written back as components (runs packed as first, -last). Components have no number and no facet form, so the tag is lost and side regions are dropped. Since v16.3.0; see doc/formats/ansysinp.md.
ansys_rst📖📖❌❌Read only: the result file's node and element components → point and cell regions, as for .cdb; ansys_rst_cyclic keeps a region's members in every sector of the full rotor. See doc/formats/ansys_rst.md.
code_aster✅✅❌❌GROUP_NO / GROUP_MA are named node and element groups with no number, so the tag is lost; a name used by both is two regions. A .mail mesh has no facet group, so side regions are dropped on write. Names are sanitised to 24 letters, digits and _ on write.
patran✅✅❌✅A packet 21 named component lists nodes and elements: its node entries are a point region and its element entries a cell region of the same name, both tagged with the component number. Elements no component names are grouped by property into property_<pid> regions (tag = pid). On write, a point and a cell region sharing a name are one component, numbered by a positive unused tag; component names are cut to 12 characters. No facet form. See doc/formats/patran.md.
femap✅✅❌✅A 408 group's node list is a point region and its element list a cell region of the group's title, tagged with the group id; each property is a cell region named by its 402 title (else property_<id>) and tagged with its id. On write, properties come from femap:property and their regions, other point and cell regions become groups (one per name, numbered by a positive unused tag). No facet form. See doc/formats/femap.md.
mfem❌✅↪️✅An element attribute is an attribute_<n> and a boundary attribute a boundary_<n> cell region tagged n; a v1.3 attribute set is a cell region of its name. On write, cells take mfem:attribute, else their region's tag (or a fresh number), and named regions become attribute sets. ↪️ A side region's facets are written as boundary elements, so it reads back as a cell region over a new block. No node sets, so point regions are dropped. See doc/formats/mfem.md.
libmesh✅✅✅✅A subdomain is a cell region named by the file's subdomain map (else subdomain_<id>) and tagged with its id; a side set is a side region (a side of a refined element carried to its active children), a node set a point region, both named by their maps (else boundary_<id>/nodeset_<id>) and tagged. Since v16.11.0 an edge set is a cell region <name>:edge over line cells made from the edges, and a shell-face set a cell region <name>:shellface0/1. The writer (v16.11.0) maps each of these back, other cell regions to subdomains. See doc/formats/libmesh.md.
abaqus_fil✅✅❌❌The results file's node sets (1931/1932) and element sets (1933/1934) ↔ point and cell regions (written since the .fil writer joined); a name longer than 8 characters is resolved through the 1940 labels. Surfaces are not in the .fil file. See doc/formats/abaqus_fil.md.
radioss✅✅✅✅A /PART is a cell region named by its title and tagged with its id, a /SUBSET the cell region of its parts, /GRNOD a point region, /GRBRIC//GRSHEL/… cell regions (since v16.11.0 also by BOX, BOX2, GENE and GEN_INCR) and /SURF/SEG a side region (since v16.11.0 also /SURF/PART, SUBSET, MAT, PROP, GRBRIC, GRSHEL, GRSH3N, GRTRIA and SURF), each tagged with its id. Written since v16.17.0: parts from radioss:part (else a cell region per part), a cell region that is a union of whole parts as a /SUBSET, any other as one group per element family, point regions as /GRNOD/NODE and side regions as /SURF/SEG, ids from a positive free tag. See doc/formats/radioss.md.
radioss_anim❌📖❌✅Read only (v16.11.0): each part of an animation file is a cell region named by its title and tagged with its id. See doc/formats/radioss_anim.md.
marc✅✅❌❌A deck's DEFINE ELEMENT SET is a cell region and DEFINE NODE SET a point region, named as the deck names them (ranges, set names and AND/EXCEPT/INTERSECT resolved); edge and face sets are kept as field data (v16.12.0). The post file's element and node sets (marc_t19, read only) alike. Written since v16.17.0 as DEFINE NODE SET/DEFINE ELEMENT SET (names made one token); side regions are dropped with a warning. See doc/formats/marc.md.
lsdyna_d3plot❌📖❌✅Read only: every part with elements is a cell region named by its title (else Part <id>) and tagged with the user part id; the d3plot has no sets. See doc/formats/lsdyna_d3plot.md.
nastran_op2 / nastran_h5❌📖❌✅Read only: one cell region per property id, named <PTYPE>_<pid> after the property card (PID_<pid> when none defines it) and tagged with the pid; an OP2 read through its sibling deck has the deck's regions instead. See doc/formats/nastran_op2.md.
z88✅✅❌✅A Z88OS structure file has no groups. Z88Aurora's z88sets.txt (read since v16.11.0, written since v16.12.0): element sets are cell regions and node sets point regions, named as in Aurora and tagged with the set id (surface sets are skipped). On write, cell regions become element sets (purpose MATERIAL) and point regions node sets (CONSTRAINT), the id a positive unused tag or the next free one; side regions are dropped with a warning. See doc/formats/z88.md.
nastran❌✅❌✅A HyperMesh component ($HMMOVE <id> lists its element ids, $HMNAME COMP <id>"name" names it; an element no $HMMOVE lists joins the component whose recorded property is its PID) is a cell region tagged with the component id; an OptiStruct SET,<id>,GRID|ELEM,LIST card is a point or cell region tagged with the set id, named by $HMSET. On write, pairwise-disjoint cell regions become components (comments only, so no solver sees them), keeping a positive unique tag as the id; point, side and overlapping regions are dropped. See doc/formats/nastran.md.
mphtxt / mphbin❌✅❌❌A COMSOL Selection lists the geometric entities of one dimension; it reads as the cell region of that dimension's elements on those entities, with the selection's dimension. On write, the entities are numbered after the disjoint cell regions, so every cell region that is a union of whole entities becomes a Selection; others, and point and side regions, are dropped. Selections have no number, so the tag is lost. See doc/formats/mphtxt.md.
gmsh 2.2❌✅❌✅A physical group is a named, tagged, per-dimension group of elements. No node-set and no side-set concept. A region with no gmsh tag of its own (Abaqus/MED/MDPA origin) gets one freshly allocated rather than being dropped (v11.5.0, roadmap §1 tier B3); the per-element tag column has no block-alignment restriction, unlike 4.1's $Entities.
gmsh 4.1❌✅❌✅Membership lives in $Entities, which describes the geometry — so it round-trips (since v9.7.0) for a mesh that says which entity each node belongs to, i.e. one carrying point_data["gmsh:dim_tags"], as any mesh read from a 4.1 file does. A mesh that came from another format has no entity structure of its own, but one is now synthesized from Cell regions (v11.5.0, roadmap §1 tier B3), with one entity per block and tags unique within each dimension, so a region spanning a triangle and a quad block gets two entities and a mixed mesh with no regions writes too; a block that several regions share is split into one block per region first (v17.17.0), so each region becomes an entity of its own and reads back whole, its cells grouped by region (2.2 keeps their original order).
exodus📖📖📖✅Read only (v8.6.0): element blocks → cell, node sets → point, side sets → side, each tagged with its eb_prop1/ns_prop1/ss_prop1 id. The writer emits neither eb_names nor side sets, so nothing written comes back. See doc/formats/exodus.md.
flac3d❌🏷️❌❌A ZGROUP/FGROUP is a named group of zone/face cells. FLAC3D identifies a group by all three of (ZGROUP vs FGROUP, name, slot), so the region is named `<zone
vtkhdf❌✅❌✅A partitioned file reads as one cell region per piece (piece_<i>), a composite as one per block (named by its Assembly link; a partitioned block's pieces are <block>/piece_<j>), each tagged with its position in the file. On write, a composite type carves the mesh along its cell regions when they partition the cells and writes the blocks in (tag, name) order — the mesh stores regions sorted by name, so the tag is what carries the order. A plain UnstructuredGrid write does not carry regions. See VTKHDF.
pvtu / pvtp✅✅✅✅Every piece of the index becomes one cell region named piece_<i> (a file with a single piece attaches none, and piece=k keeps one piece with none). The writer carves by the partition:part cell_data array, not by regions; since v16.27.0 each piece carries its part of the mesh's regions in the VTK XML convention, and reading combines same-named regions back into one. See PVTU.
pvd✅✅✅✅The entries of the chosen step become one cell region each, named from name=, else group/part_<p>, else part_<p>; a step that is a single entry is returned as that file reads (a .pvtu keeps its own piece_<i> regions). The entries' own regions (the VTK XML convention, since v16.27.0) are combined by name. See PVD.
med✅✅❌❌A MED family (FAS/NOEUD/FAS/ELEME) is a named group of nodes/elements — one region per group name, a multi-group family contributing to all of its names. A family id is per unique combination of names rather than per name, and a name may span several ids, so the format-native tag is not carried. MED has no facet-group concept, so side regions are dropped. See doc/formats/med.md.
openfoam✅✅✅❌pointZones/cellZones/faceZones (v11.4.0, roadmap §1 tier B2) — each zone is a Region of the matching kind; ids are OpenFOAM's own (point/cell ids pass straight through, a face id becomes (global cell, local facet) via its owner cell). flipMap is never read or written — a Side entry carries no orientation bit to hold it in. Boundary patches are a separate mechanism (cell_tags + OpenFoamInfo, unchanged) and are not regions. See doc/formats/openfoam.md.
su2❌✅¹❌✅A named (non-numeric) MARKER_TAG boundary is a cell region — "<name>", or "zone_<i>/<name>" for a multizone file, which also gets one "zone_<i>" region per zone. The numeric su2:tag round-trips exactly either way. ¹ Only a region over boundary-typed cells can be written back as a marker — SU2 has no per-volume-cell tag at all, so a region over volume cells is silently dropped on write. No node-set and no facet-group concept, so point and side regions are dropped. See doc/formats/su2.md.
unv✅✅❌✅¹A permanent group (datasets 2467/2477/2452/2435 and the pair layouts 2417/2429/2430/2432) lists nodes (entity type 7) and elements (type 8): its nodes become a point region and its elements a cell region of the same name, both tagged with the group number (v15.6.0, roadmap §1.1; the UnvInfo sets are filled too). On write, a point and a cell region sharing a name are one 2467 group. ¹ The group number is a positive, unused tag; an untagged region gets the next free number. No facet-group concept, so side regions are dropped. See doc/formats/unv.md.
tecplot❌✅❌❌Reading a non-transient multi-ZONE file (v15.5.0, roadmap §1.1; ASCII and, since v16.10.0, binary .plt) gives one cell region per zone, named from the zone's own title (de-duplicated) or zone_<i> when it has none; the region's tag is the zone's own position in the file, but that position is not meaningful on write, so tag does not round-trip. On write, a cell region whose entries exactly match one cell block's global range supplies that block's zone title; a block with no matching region gets block_<i>. No node-set and no facet-group concept, so point and side regions are dropped. See doc/formats/tecplot.md.

📖 = read only. Exodus is a region source rather than a round-trip target, so it is recorded in tests/python/test_region_roundtrip.py's READ_ONLY_REGIONS rather than as a row in the round-trip matrix — that matrix writes and reads back, and a format that cannot write cannot round-trip.

↪️ = kept, but as the format's own entities: Elmer has no facet group, so a side region's facets become boundary elements and come back as a cell region over new cells. Recorded in tests/python/test_region_roundtrip.py's FACETS_BECOME_CELLS bucket, since those new cells change the geometry the matrix compares.

🏷️ = membership round-trips but the name is rewritten into the format's own namespace. FLAC3D is the one such format, recorded in that file's NAMESPACED_REGIONS bucket for the same reason: the matrix asserts that a region's name survives exactly, which is the right assertion for every format that can make it, and weakening it for one exception would cost the other three.

Exodus's writer is the one gap left: it writes element blocks but no node or side sets, so only cell regions round-trip through it (roadmap §4).

SU2 is not in test_region_roundtrip.py's executable matrices. That file's shared fixture carries one point, one cell (a volume-cell region) and one side region; SU2 supports none of the first and last, and, as the table above spells out, only a boundary-typed cell region round-trips — so the shared fixture cannot exercise the behavior this format actually has either way. SU2's marker-name mapping is instead covered directly, with boundary-shaped fixtures, in tests/python/test_su2.py and tests/cpp/test_misc_formats.cpp.

Gmsh precedence ​

The gmsh mapping is derived and additive, never authoritative:

  • Read: regions are derived from the existing gmsh:physical cell_data plus the $PhysicalNames entries in field_data. Both stay exactly as they were, so every existing consumer is unaffected.
  • Write: field_data and gmsh:physical are consulted first; regions only fill gaps. A mesh carrying gmsh's own metadata therefore writes byte-identical bytes, and a mesh whose groups came from another format still gets real physical groups.

A gmsh dimension-0 physical group tags vertex cells, not points, so it becomes a "cell" region with dim == 0 rather than a "point" one.

XDMF sets ​

For XDMF time series, sets are stored once on the shared mesh grid and inherited by every step. Native WritePointsCells(mesh) persists the mesh's regions; Python's write_points_cells(points, cells, regions=mesh.regions) does the same and reader.regions exposes them after read_points_cells(). Membership is fixed, including empty groups; WriteData does not replace it with the step mesh's regions. XML, Binary and HDF use the same point/cell/side encoding as single-grid files.

A region is one <Set Name="…"> in the grid. A point region is a SetType="Node" set of point indices and a cell region a SetType="Cell" set of cell indices, in file order, which is block-major. A side region uses the XDMF model's own layout for a Face or Edge set: the first <DataItem> holds the cell indices and the second the cell-local face or edge indices. meshio++ numbers facets its own way (see the numbering above), since the XDMF model leaves that to the producer. A side region that spans 3-D and 2-D cells is written as one Face set and one Edge set with the same name, and read back as one region. dim and tag ride in <Information Name="meshio++:dim" Value="…"/> and meshio++:tag children, written only when not -1. A Grid set, or an attribute on a set, is skipped with a warning.

The VTK XML convention ​

VTK has no named-set concept, so each region is a dataset-level Int64 array in <FieldData>:

ArrayContents
region:point:<name>point indices, one component
region:cell:<name>cell indices, one component
region:side:<name>(cell, local facet) pairs, two components
region-meta:<kind>:<name>[dim, tag], written only when either is not -1

Cell indices are in the file's cell order, which is how every VTK tool numbers cells. A .vtp file stores its cells as Verts, then Lines, then Polys, so its writer translates. The .vtu reader buckets polyhedra by node count, so it translates back. On read, the arrays are removed from field_data, and from the field-data names a metadata read reports. They are read whatever arrays= asks for, since they describe topology rather than data. An array that does not fit the convention (wrong dtype, wrong component count, an index out of range) is left in field_data with a warning. A field_data entry whose name uses a region: prefix is not written, with a warning. ParaView shows these arrays in its spreadsheet view and keeps them when it saves the file again.

The pieces of a .pvtu, .pvtp or .pvd each carry their own part of a region. When the pieces are read back into one mesh, same-named regions are combined into one, rather than kept apart as 0:wall, 1:wall the way merge keeps unrelated meshes' regions.

Abaqus face identifiers ​

Abaqus numbers element faces S1..S6, and that numbering is not meshio++'s. C3D8's S1 is the 1-2-3-4 face — local nodes {0,1,2,3} — which is meshio++'s face 4. The mapping is spelled out per type in both abaqus.cpp (abq_face_index) and _abaqus.py (_ABAQUS_FACE_ORDER), which are twins and must stay in step: getting it wrong yields a plausible-looking side set pointing at the wrong faces. Shell elements' SPOS/SNEG name a side rather than a facet and map to facet 0 and 1.

How operations treat regions ​

A mesh with two named regions before cropping

The same mesh after cropping, its regions remapped onto the kept cells

OperationsTreatment
crop, split, merge, reorder, clean, partition, convert_cells, refine, decimate, subdivide, agglomerate, undo_green, repairRemapped through detail/region_remap.hpp, side regions included (since v16.27.0)
transform, smooth, interpolate (the target's), the data operations, attach_qualityPassed through — nothing is renumbered
extract_surface, extract_skinA side region becomes a cell region of the surface, naming the facets it covered; point regions follow the kept points; cell regions are dropped with a warning (since v16.27.0)
slice, isosurfaceDropped, with a log::warn naming the operation

merge namespaces colliding names by source index (0:wall, 1:wall), the same rule it already applies to field_data keys.

detail/region_remap.hpp is the single owner of the carry. It needs to be told which shape an operation's cell map is in, because they genuinely differ:

  • Direct — map[c] is the output cell, or -1. (crop, split, clean, partition, reorder.)
  • FirstChild — map[c] is the first of a contiguous run of children, ending at the next non-negative entry. (convert_cells' simplexify, refine, decimate.) This shape also handles 1:1 maps correctly, but Direct must not be replaced by it: a permutation's map is not monotone, so the "next entry" rule would invent nonsense ranges.
  • Global — one flat array indexed by input global cell index. (merge.)

A side entry keeps its local facet number while its cell keeps its identity: one output cell of the same type, reached by no other input cell, whose facet at that number still has the same nodes. A cell whose winding was reversed renumbers its facets, which is why the nodes are checked too. Otherwise, since v16.27.0, the facet is found again by the nodes it is made of:

  • Refined (the cell has several children, as with subdivide, refine and convert_cells' simplexify): the entry becomes every child facet lying within the original facet. Each node of the child facet is either an image of one of the facet's nodes, or a point the operation created that lies on it.
  • Merged or retyped (one output cell that other input cells also map to, as with agglomerate and undo_green, or one of another type, as with linearize and elevate): the entry becomes the output facet containing the original facet. Each surviving node is one of that facet's nodes, and each node the operation removed lies on it.
  • Moved to a neighbour (a FirstChild map only, as with decimate and repair): when neither search finds the facet but all its corners survive, it is looked up across the whole output by those corners, since a decimation collapse hands a boundary edge to a neighbouring triangle.

Node identity decides wherever it can. Geometry (a 1e-9 tolerance, relative to the facet's size, on the facet's plane or segment) only places the points an operation created or removed, which are averages of the facet's own corners. An entry with no counterpart is dropped, with one warning per region counting the dropped entries. Polygon edges and polyhedron faces are facets too: edge k of a polygon runs from its node k to node k + 1, and facet k of a polyhedron is its k-th face. RegionRemap::mDropSideRegions still drops side regions by name for an operation that asks. The numpy fallbacks apply the same rules (_side_carry.py).

A region that loses every entry is still carried, as an empty group — the name is information in its own right, and that is what the per-operation Python shims did before the shared helper replaced them.

The other language bindings ​

C API (doc/c_api.md) — an opaque snapshot handle:

c
mio_regions* r = mio_regions_create(mesh);
mio_region_info info;
mio_regions_info(r, 0, &info);
int64_t count;
const int64_t* entries = mio_regions_entries(r, 0, &count);
mio_regions_free(r);

mio_mesh_add_region(mesh, "wall", MIO_REGION_SIDE, 2, -1, pairs, 4);

Fortran (doc/fortran.md) — m%regions(keys=, entries=) and m%add_region(...). Point and cell indices are shifted to Fortran's 1-based convention by the copying getter; the facet column of a side region is not, matching the partition_labels rule that a value which is not an index stays as it is.

WebAssembly (doc/wasm.md) — regions ride on the mesh object, so readMesh / writeMesh / convert carry them with no new call:

js
const mesh = mio.readMesh('/in.inp');
mesh.regions; // [{ name, kind, dim, tag, entries }]

Native CLI — meshioplusplus info prints Point sets: / Cell sets: / Side sets:, and meshioplusplus diff reports regions added, removed and changed, folding them into its nonzero exit code. (meshes_equal deliberately does not consider them: it is documented to compare geometry and data.)

Editing regions ​

edit_regions(mesh, edits) (v16.23.0) applies a list of edits, in order, to a copy of the mesh's regions, leaving points, cells, data and property sets untouched. It is what a consumer building boundary conditions otherwise hand-rolls: "the inlet is these two gmsh groups", "the wall is the skin minus the inlet and outlet", "call it Inlet and give it tag 10".

python
import meshioplusplus as mp

mesh = mp.edit_regions(mesh, [
    {"op": "union", "inputs": ["inlet_a", "inlet_b"], "output": "inlet"},
    {"op": "difference", "inputs": ["skin", "inlet", "outlet"], "output": "wall"},
    {"op": "rename", "inputs": ["Surface 3"], "output": "Outlet"},
    {"op": "retag", "inputs": [{"name": "wall", "kind": "side"}], "tag": 10},
    {"op": "delete", "inputs": ["scratch"]},
])
EditInputsResult
union, intersection, differencetwo or more, one kinda region named output (difference is the first minus the rest); the inputs stay unless keep_inputs is false
renameonethe region moved to output, keeping its kind, entries, dimension and tag
retagonethe region with a new tag and/or dim (and name, if output is given)
deleteone or morethe inputs removed

Selecting a region. A region's identity is (kind, name, dim, tag), and a file may carry one name twice — gmsh allows a physical name per dimension. An input is a name, or {"name", "kind", "dim", "tag"} pinning the rest, and must match exactly one region: an ambiguous or missing match raises with the candidates listed, rather than picking the first.

The results. Set operations work within one kind — side regions combine their (cell, facet) pairs — and mixing kinds is an error. A set operation's result has the inputs' shared dimension (-1 when they differ) and tag -1 unless dim / tag are given, and an empty result is kept: a named group is information. An edit whose result would overwrite an existing region other than one of its own inputs is an error. For gmsh output, note that a cell's physical tag comes from the gmsh:physical cell data when the mesh carries it (Gmsh precedence), so retagging a cell region of such a mesh warns that the array still holds the old tag.

The uniform C++ API gained Mesh::RemoveRegion(i) for this (renaming or retagging is a remove plus an add, since the name and tag are part of the key); operations/region_ops.hpp declares edit_regions, find_region and the RegionSelector/RegionEdit types. The Python implementation edits the region list directly rather than round-tripping the mesh through the core, and is pinned to _core.edit_regions. Every other surface carries it:

  • CLI (both) — edit flags on the regions verb, applied in command-line order, with an output file: meshioplusplus regions in.msh out.msh --union inlet=inlet_a,inlet_b --difference wall=skin,inlet,outlet --rename "Surface 3=Outlet" --retag side:wall=10 --delete scratch (--retag NAME=TAG[:DIM]; --drop-inputs makes the set operations remove their inputs; prefix a name with point:, cell: or side: to pin its kind).
  • Pipeline — one edit per step: {"Op": "EditRegions", "Edit": "union", "Inputs": ["a", "b"], "Output": "ab", "Kind": "cell", "Dim": 2, "Tag": 7, "KeepInputs": true}.
  • C — mio_edit_regions(mesh, MIO_REGION_UNION, selectors, n, "out", MIO_REGION_ANY, MIO_REGION_ANY, 1) with mio_region_selector (initialized by mio_region_selector_init), and mio_mesh_remove_region(mesh, index); Fortran m%edit_regions('union', ['a', 'b'], output='ab') / m%remove_region(i); Julia edit_regions(m, :union, ["a", "b"]; output="ab") / remove_region!(m, i); R mio_edit_regions(m, "union", c("a", "b"), output = "ab") / mio_remove_region(m, i); WASM editRegions(mesh, [{op, inputs, output, dim, tag, keepInputs}]); MCP the edit_regions tool.

The same selector names the two regions of periodic node matching.

Enumerating and splitting by region ​

Two things build directly on the model above, both added in v8.7.0:

  • read_metadata(...)["regions"] (Python; readMetadata(...).regions in WASM; mio_read_metadata_num_regions/_region_name/_region_info in the C API; mio_metadata%regions in Fortran; the equivalent shape in Julia/R) — each region's name/kind/dim/tag/num_entries, without the entries themselves. Populated from whatever's already on an in-memory mesh, so it costs nothing extra whenever the summary came from a fallback read (every format lacking a native metadata path, plus Exodus, which always falls back); the VTU/VTP/XDMF native metadata paths report them too (since v16.27.0) from the arrays' and sets' declared sizes, with dim/tag -1 for VTU/VTP, since those live in the payload. Gmsh 4.1's native path does report them (since v9.7.0): $Entities and $PhysicalNames are both small and both precede $Elements, so the group names, dimensions, tags and entry counts come out of the block headers alone, and the summary agrees with a real read. This is what lets a caller build a SubModelPart tree — or just decide whether it's worth reading the mesh at all — without paying for a full read first.

  • meshioplusplus regions FILE (both CLIs) lists the same summary directly (--json for machine consumption):

    bash
    meshioplusplus regions bracket.inp
    # <meshio++ mesh regions> (2)
    #   fixed (point, 12 entries, tag=1)
    #   solid (cell, 340 entries, dim=3, tag=2)
  • split(mesh, by="regions") (see doc/split.md) turns each named Cell region into its own submesh — the natural next step after listing them. Unlike every other split criterion this is not a partition: regions may overlap, so a cell can land in zero, one, or several output pieces; Point/Side regions produce no piece, since there is no sound default for "these facets alone".

Nested groups: the / convention ​

Regions are flat — a Region has a name, not a parent. Formats with genuinely nested groups (Kratos SubModelParts, and MDPA's nested Begin SubModelPart) therefore flatten the path into the name with /:

Structure/Loads/PointLoad3D

This is a convention, not a parser: the region is one name that happens to contain slashes, and it round-trips as such. The KRATOS mesh backend is the one place that interprets it — BuildSubModelPartsFromRegions splits on / and walks or creates the chain, adding members to the leaf, and RestoreRegions walks the tree back into the same flattened names.

. is reserved and cannot appear in a segment: ModelPart::FullName() joins ancestors with it. A region name containing one is left on the mesh but not materialized as a SubModelPart, with a warning — the same treatment Side regions already get. This is the exact interaction MED's own Kratos MedApplication runs into: it uses . as its group-name nesting separator when writing MED family names from Kratos, so a MED file carrying such a name round-trips its region unaffected through every mesh backend except KRATOS's SubModelPart materialization, which — per this rule — leaves it flat with a warning rather than guessing at the intended nesting.

One thing a SubModelPart cannot carry back is a region's mDim/mTag: it stores a name and member ids and nothing else. A region reconstructed from one reports -1 for both unless a staged region of the same name and kind supplied them. That is the truthful answer rather than a gap — synthesizing a value would be inventing data.

Released under the MIT License.