Skip to content

OpenFOAM polyMesh ​

A reader and writer for OpenFOAM's native polyMesh representation: an unstructured, face-based mesh described by 4-5 sibling files (points, faces, owner, neighbour, boundary) rather than a single file. Supports both ASCII and binary (little-endian, label=32/64, scalar=32/64) encodings, and reconstructs general polyhedra as well as tetra/pyramid/wedge/hexahedron.

Format nameopenfoam
Extensions.foam
Read / Write✓ / ✓
Extra dependencies—

Reading & writing ​

Native vector/nonuniform field extraction uses shared-stream views of the live source instead of first copying the entire text slice. Result fields remain owned and parsing/format behavior is unchanged. The source-copy evidence times geometry-only meshes; field paths are fixture-tested, not a measured field speedup.

python
import meshioplusplus

mesh = meshioplusplus.read("case.foam")                 # a <case>/case.foam marker file
mesh = meshioplusplus.openfoam.read("/path/to/case")     # or the case directory directly
mesh = meshioplusplus.openfoam.read("/path/to/constant/polyMesh")  # or polyMesh directly
mesh = meshioplusplus.read("/path/to/case")               # v16.2.0: the directory is sniffed, no format needed
python
meshioplusplus.write("out/case.foam", mesh)              # infers the format from `.foam`
meshioplusplus.openfoam.write("/path/to/case", mesh)     # a case root (no extension)
meshioplusplus.openfoam.write("out/case.foam", mesh, binary=True)  # v15.5.0 -- see "Binary write"

read(filename) takes no keyword arguments; the polyMesh directory is located from whichever of the three input forms above is given (_resolve_polymesh): a .foam suffix looks for <parent>/constant/polyMesh; a directory literally named polyMesh is used as-is; any other directory is checked for constant/polyMesh then polyMesh as subdirectories. A FileNotFoundError is raised if none match.

File structure ​

Each polyMesh file (points, faces, owner, neighbour, boundary) is an OpenFOAM "FoamFile": a header block declaring format (ascii/binary) and (for binary) an arch string encoding label=32|64/scalar=32|64, followed by a count and a parenthesized list.

FoamFile
{
    ...
    format      binary;
    arch        "LSB;label=32;scalar=64;";
}
// ...
<N>
(
<binary or ascii data>
)
  • Header detection (_detect_format) scans line-by-line for a format ...; line and an arch "..."; line, extracting label= / scalar= byte widths; defaults to ascii/8-byte if absent. A non-LSB arch (a big-endian source) is refused by name, not silently misread — binary bytes this reader decodes are always little-endian.
  • Binary points (vectorField): N ( followed directly by N*3*scalar_bytes raw floats, no per-row framing — read via a single np.frombuffer.
  • Binary owner/neighbour (labelList): N ( followed by N*label_bytes raw ints, same direct-buffer read.
  • Binary faces (faceList, non-contiguous): each face is its own labelList — <count> ( <count*label_bytes bytes> ) repeated N times, not CompactListList/faceCompactList (the offsets-plus-flat-data encoding some other OpenFOAM tooling uses for a compact binary face list). Read in two passes: a sequential ASCII scan locating each face's byte offset and node count (cheap, since find(b"(") only ever scans the short ASCII gap between faces, never binary data that might coincidentally equal '('), then a single vectorized gather of every face's binary blob via a cumulative byte mask — bounded peak memory even for tens of millions of faces (see _RaggedArray, a CSR-style (conn, offsets) pair standing in for a list[list[int]]). Binary write emits exactly this per-face shape, matching the reader it must round-trip through.
  • Binary cellZones/faceZones/pointZones: the same per-zone name { type ...; <key> List<label> N(...); } dict structure as ASCII, with the List<label> payload as raw bytes. The dict structure around it (names, braces, the type/keyword text) stays plain text even in a binary file, and a raw little-endian id can easily contain a byte equal to {, } or / (a small id's low byte routinely does), so the C++ reader parses this directly off the raw file bytes rather than through the shared comment-stripping pass every other file body goes through — see parse_zone_file_binary's own doc comment in formats/openfoam.cpp.
  • ASCII variants use simple regex/line-based parsing (_parse_points_ascii, _parse_faces_ascii, _parse_int_list_ascii) after comment-stripping (/* */ and //) and header-skipping.
  • boundary: a dict of patch_name → {type, nFaces, startFace}, parsed via a brace-matching regex over the whole (header/comment-stripped) text.
  • Cell reconstruction: cell↔face topology is built once as a CSR _RaggedArray (_cell_faces_csr, vectorized via argsort+bincount) from owner/neighbour; per cell, each face is oriented outward (reversed if the cell is that face's neighbour, since the stored normal points owner→neighbour) and classified by (n_faces, n_points): (4,4)→tetra, (5,5)→pyramid, (5,6)→wedge, (6,8)→hexahedron, anything else → a general polyhedron (kept as outward-oriented face lists). Each of the 4 named types has a dedicated orientation-fixing builder (_build_tetra/_build_pyramid/_build_wedge/_build_hexahedron) that computes a scalar triple product and flips the node order if it comes out negative, guaranteeing positive-volume connectivity regardless of the source mesh's face-normal convention.

Cell types ​

Volume cells: tetra, pyramid, wedge, hexahedron, and general polyhedron<N> (grouped by unique node count N, ragged per-cell face lists stored as an object-dtype array — one CellBlock per distinct N).

Boundary (patch) faces: triangle, quad, and polygon<N> for N > 4 (grouped by vertex count N, one CellBlock per size — via _build_boundary_polygons).

Data mapping ​

  • cell_data["cell_tags"] — per-cell-block tag array: 0 for every volume cell block, and a distinct negative "MED-style family id" -(patch_index+1) per boundary patch's face blocks (so a triangle patch and a quad patch on the same physical boundary would currently get different tag values — see Quirks).
  • mesh.cell_tags — mesh-level attribute (not cell_data), {family_id: [patch_name]}, letting a MED write bridge these patch names through the same mechanism used for Gmsh physical groups (see med.md).
  • mesh.point_tags — always set to {} (present for interface symmetry with the MED-derived tag convention; OpenFOAM has no point-tag concept).
  • No field_data. point_data/cell_data come from a selected time directory's field files (U, p, T, …) when requested via time_step/arrays — see "Time-directory fields" below; the plain read() (no time_step/arrays) still attaches time-zero's fields by default, matching every other format's "data on unless opted out" convention.

Zones as named regions ​

cellZones/faceZones/pointZones (v11.4.0, tier B2) round-trip as mesh.regions — see doc/regions.md for the model.

python
mesh = meshioplusplus.read("case.foam")
for r in mesh.regions:
    print(r.name, r.kind)  # e.g. "porousZone" "cell", "inlet" "side"
  • cellZones → one Region of kind "cell" per zone, entries the zone's cells as global (block-major) indices.
  • pointZones → one Region of kind "point" per zone, entries the zone's point indices, unchanged (points are never reordered by this reader/writer).
  • faceZones → one Region of kind "side" per zone; a face id becomes (global cell, local facet) via its owner cell (the same cell OpenFOAM's own owner array names) — flipMap is never read or written, since a Side entry carries no orientation bit to hold it in. A zone member coinciding with a skipped (degenerate) cell is dropped, logged as a count.
  • The facet half of a faceZones entry is not numerically stable across a round trip. The reader rebuilds each named cell type (tetra/pyramid/wedge/hexahedron) from face topology alone, which is free to relabel local nodes — so the same geometric face can come back at a different local facet index. The cell half, and the geometric face itself (its corner point ids), are unaffected. tests/cpp/test_openfoam.cpp's ZonesRoundTripAsNamedRegions asserts exactly this invariant; tests/python/test_region_roundtrip.py's openfoam row documents it as the one exception to that table's usual exact-entries assertion.
  • Boundary patches (cell_tags/mesh.openfoam_patch_types, the boundary file) are a separate, older mechanism and are not regions — a patch is not a faceZone, and this tier does not change that.
  • The writer only produces cellZones/faceZones/pointZones once the mesh carries a Region of the matching kind; otherwise a stale file from a previous write is removed, the same "leave no stale companion behind" rule the writer already applies to neighbour/boundary.
  • C++ core only, like the writer itself: the pure-Python fallback reader (_openfoam.py) does not read zone files, matching the precedent that this module's more advanced pieces (the writer entirely) ship compiled-only rather than duplicating the per-cell winding/topology logic in a second implementation.

Multi-region cases ​

A multi-region case has no single constant/polyMesh; each region has its own constant/<region>/polyMesh, listed in constant/regionProperties. Select one with region=:

python
mesh = meshioplusplus.openfoam.read("case.foam", region="fluid")

Reading a multi-region case with no region raises, naming the regions found under constant/ (v11.4.0, tier B2) — it does not silently try (and fail to find) a bare constant/polyMesh. Reading a region's polyMesh directory directly (case/constant/fluid/polyMesh) needs no region at all — the plain polyMesh-directory resolution rule already covers it. region is C++-core only (OpenFoamInfo::mRegion) and read-side only; a multi-region write is a documented follow-up, and the pure-Python fallback reader has no multi-region concept, so a region request — or a case the compiled core recognised as multi-region — re-raises rather than silently falling back to a worse error.

Decomposed cases ​

A decomposed case has no constant/polyMesh at all, only processor0/constant/polyMesh, processor1/constant/polyMesh, … (decomposePar's own layout). read detects this — no single-region polyMesh and no region selected, but processorN directories present — and reconstructs one mesh with the original global numbering, mirroring what OpenFOAM's own reconstructParMesh does on disk (v11.4.0, tier B2):

python
mesh = meshioplusplus.openfoam.read("case.foam")  # transparent -- no extra argument

Each processor's own pointProcAddressing/cellProcAddressing/faceProcAddressing (plain labelLists) map its local ids back onto the global ones; faceProcAddressing's sign says whether a processor's local copy of a face is stored reversed relative to the global orientation. A global face claimed by exactly one processor is either interior to it or a real exterior boundary face; claimed by two, it is a genuine internal face decomposePar split at a processor boundary — the positive-signed entry names the true owner side, the negative-signed one the neighbour side. A boundary face's global patch comes from its owning processor's boundaryProcAddressing; a processor* inter-rank patch (missing/negative addressing, or a type starting with processor) has no counterpart in the original case and is dropped.

The pure-Python fallback reader has no concept of processor*/ directories. A decomposed case is also written (decomposed=True, see Writing decomposed cases), and its processorN/<time>/ fields are read back through the same addressing: cell fields through cellProcAddressing, point fields through pointProcAddressing, NaN where no processor has a value; the patch values of the boundary-face blocks follow each reconstructed patch face back to the processor that owns it.

Writing multi-region cases ​

write(path, mesh, region="fluid") writes constant/fluid/polyMesh (and the fields under <time>/fluid/); call it once per region on the same case path. constant/regionProperties, which a solver reads for its region families, is written only on request (below); the reader finds the regions as the constant/<name>/polyMesh directories either way. read(path, region="fluid") reads one back, fields included; without region it raises naming the regions.

regionProperties ​

write(path, mesh, region="water", region_family="fluid") also lists the region under a family in constant/regionProperties: an existing file is read, the region is added to the family (the family is created last if new; a region already listed under another family is an error, and a region written twice is listed once), and the file is written again. write_region_properties(path, {"fluid": ["water", "air"], "solid": ["heater"]}) writes the whole file, and read_region_properties(path) returns it as {family: [regions]} in file order. The C++ counterparts are write_openfoam_region_properties and read_openfoam_region_properties with OpenFoamRegionGroup {mFamily, mRegions}.

The file is the regions ( fluid (water air) solid (heater) ); list of chtMultiRegionFoam-style solvers:

regions
(
    fluid (water air)
    solid (heater)
);

meshio++ has no solver-neutral content for this file, so it does not invent any: the family names are the caller's and are written verbatim, nothing else (no controlDict or regionSolvers entry) is written, and without region_family no file is made. Names are validated as one unquoted word (no whitespace or any of (){};"'#$/\), a region may not start with a dot, and a family or region may not repeat. The older fluidRegionNames/solidRegionNames keys are not read, and a hand-written file's other entries are lost when region_family rewrites it. The reader needs no file; when a multi-region case has no region directory yet, the error from reading it with no region names the regions regionProperties lists. Compiled core only.

Writing decomposed cases ​

write(path, mesh, decomposed=True) writes decomposePar's layout for a mesh carrying cell_data["partition:part"] (meshioplusplus.partition_labels): for each processor processorN/constant/polyMesh with its cells' faces (internal faces first, then the original patches, then one processor patch per neighbouring processor with myProcNo/neighbProcNo; a face shared with a neighbour is stored reversed on its neighbour side, as faceProcAddressing's negative sign records), pointProcAddressing, cellProcAddressing, faceProcAddressing and boundaryProcAddressing (-1 for the processor patches), and each processor's share of the fields in processorN/0/. The global numbering is the one the serial write produces, so reading the case back gives the serial mesh. Write into a fresh directory: a serial constant/polyMesh beside the processors is the one a reader prefers.

Zones in decomposed cases ​

A mesh's zone regions (cell, point and side kinds, see Zones as named regions) are decomposed with the mesh: every processor gets constant/polyMesh/cellZones, faceZones and pointZones holding every zone of the mesh (an empty list where the processor has no member, as decomposePar writes them), in that processor's own ids. A cell is listed by the processor that owns it, a point by every processor whose faces use it, and a face by every processor that holds a copy, so a face on a processor boundary appears in both zones. The reader merges the processors' lists back through cellProcAddressing, pointProcAddressing and faceProcAddressing (a face's sign is ignored: flipMap is neither read nor written) into one sorted, de-duplicated list per zone, in the order the zones first appear, so a decomposed case reads back the regions of the serial case; a face zone member that is a processor patch face has no counterpart in the original case and is dropped. A zone file is removed from a processor directory when the mesh no longer carries a zone of that kind. C++ core only, like the rest of the decomposed support.

Time-directory fields ​

Writing (roadmap 3.3.3): write emits point_data and cell_data as the fields of time directory 0 — volScalarField/volVectorField/volSymmTensorField/volTensorField for cell arrays of 1/3/6/9 components (in OpenFOAM's volume-cell order for internalField; the values of the boundary-face blocks become per-patch boundaryField entries, see below), pointScalarField/pointVectorField for point arrays of 1/3 — ASCII or binary like the mesh, each with a calculated catch-all boundaryField so the case loads. Arrays with any other component count, or a cell array missing from a block, are skipped with a warning. Reading takes them back as float64, scalars as one-column arrays. write_sequence(path, steps, file_format="openfoam") (or openfoam.SeriesWriter) writes a series: the polyMesh once and a time directory per step, named by the time (0.5, 1e-05; %.15g), each step over the first step's points and cells. A region's fields go to <time>/<region>/, and a decomposed case's to processorN/<time>/.

Boundary-patch values: the rows of a cell array on the boundary-face blocks (the triangles, quads and polygons that name a patch through cell_tags) are written as that patch's boundaryField entry, type calculated; value nonuniform List<T> N(...) with the faces in the patch's written order, for scalar, vector, symmTensor and tensor fields, ASCII and binary alike (binary: packed like internalField). The entries precede the ".*" catch-all, which still covers every patch without one. A patch gets values only when every one of its faces has a finite value, so the NaN the reader fills a field with when a case has no patch values writes nothing and an unchanged read-then-write leaves the field file as it was. Patches of type empty, wedge, symmetry and symmetryPlane get no values (OpenFOAM fixes their field type), and neither do the processor patches of a decomposed case or the internal faces; a decomposed case's processor keeps the values of the faces of each original patch it holds. calculated is a placeholder a solver's boundary conditions replace; the values are what the mesh data held, nothing is evaluated.

<case>/<time>/<field> dictionaries round-trip as point_data/cell_data (v11.4.0, tier B2), selected the same way every other transient format's time_step/arrays work:

python
mesh = meshioplusplus.read("case.foam", time_step=-1, arrays=["p", "U"])  # last step, two fields
meta = meshioplusplus.read_metadata("case.foam")
meta["time_values"]  # sorted values of the numeric-named time directories that hold fields
  • volScalarField/volVectorField/volSymmTensorField/volTensorField (1/3/6/9 components) become cell_data; pointScalarField/pointVectorField (1/3 components) become point_data. Any other class (surfaceScalarField, …) has no point/cell home and is skipped with a warning.
  • internalField uniform <value> expands to one row per cell/point; nonuniform List<T> is read row for row. Both ASCII and binary (the same arch-driven reader the polyMesh files use) are supported.
  • A cell field's internalField only ever covers volume cells, in OpenFOAM's own numbering — the matching cell_data blocks are the volume/polyhedron ones; the boundary-face blocks get the value of each patch's boundaryField entry (uniform, nonuniform List<T> N(...) inline or one per line, N{value}, ASCII or binary), matched to the patch by its exact name, and NaN where a patch has none (zeroGradient, empty, value $internalField, a regex entry such as ".*", a count that is not the patch's face count). A decomposed case's patch values are gathered from each processor's copy of the patch.
  • Time directories are the case root's numeric-named subdirectories that hold at least one regular file; 0 is included only when it actually holds fields. time_step is an index into that sorted list (negative counts from the end), exactly like every other transient format — not the directory's own name.
  • read_metadata(...)["time_values"] is a real, cheap native path (a directory listing, no field parsing); everything else in that summary comes from a full read, same as Exodus/EnSight.
  • C++ core only: the pure-Python fallback reader (_openfoam.py) reads no fields at all, matching the zones/multi-region/decomposed-case precedent above.

Binary write ​

Since v15.5.0 (roadmap §1.1), write/write_openfoam can write a binary polyMesh, at any of the four label/scalar width combinations the reader already accepted:

python
meshioplusplus.openfoam.write("out/case.foam", mesh, binary=True)
meshioplusplus.openfoam.write("out/case.foam", mesh, binary=True, label_bits=64, scalar_bits=32)
  • binary (default False) selects the encoding; label_bits/scalar_bits (32 or 64, default 32/64 — OpenFOAM's own default build) are only meaningful together with binary=True. Both are Python and C++ only (OpenFoamWriteOptions) — the registry's generic --binary/--ascii (both CLIs, mio_write_ex, WASM's writeMesh(..., {encoding})) always use the 32/64 default; changing the width needs the Python or C++ API directly.
  • points/owner/neighbour and zone List<label> payloads are raw little-endian bytes (count, (, bytes, )); faces is the same length-prefixed-per-face shape as ASCII, in binary — see "Binary faces" above. boundary stays plain text regardless of binary (a handful of small per-patch scalars are never worth binary-encoding, the same reasoning uniform field values use).
  • The header's format/arch lines record the encoding and widths (arch "LSB;label=32;scalar=64";), exactly what the reader's detect_format needs to parse the file back; a boundary file (still plain text) gets the same header regardless.
  • A binary request on a big-endian host raises by name rather than writing bytes that host's own reader could not parse back: OpenFOAM binary files are little-endian only, and this writer never byte-swaps.
  • Validated against foamlib (a real, independent OpenFOAM file parser — tests/python/test_openfoam.py, skipped when the package is absent) for points/owner/neighbour at the default width; foamlib has no faceCompactList/per-face-list reader of its own, so faces is cross-checked only against this project's own two engines (C++ and Python reading each other's output, and each reading its own).

Quirks & limitations ​

  • The only meshio++ writer that creates a directory. write resolves its path exactly as read does and creates <case>/constant/polyMesh/ as needed; a .foam target also gets its (empty) marker file written, which is what makes the case openable by ParaView. Because a case directory has no extension, that form needs an explicit file_format="openfoam" — resolve_format (reads) checks a few of the file's own bytes for content-aware extensions like .mesh/.dat (see MFEM) and OpenFOAM's directory sniffing, but still has no rule for a bare directory with no marker files; a write resolves by name alone, through resolve_write_format (v16.17.0), which never stats the filesystem.
  • Binary write covers the single-region, multi-region, transient and decomposed writers alike; the encoding and widths apply to every file they write.
  • There is no Python fallback writer. A twin would have to re-implement the per-cell winding repair — a discrete branch on the sign of an enclosed volume — and two implementations of such a branch can land on opposite sides for a near-degenerate cell, the same reasoning that keeps smooth's inversion guard out of its numpy fallback. It would also be dead code: openfoam_write needs no optional dependency, so it ships in every wheel. The writer is registered only when the compiled core provides it.
  • Face ids are not preserved by a round trip. They come from meshio++'s own cell-major deduplication, not from the source file's face order. The invariant the writer guarantees is topological (the ordering contract below), not byte-level.
  • A patch's type is downgraded when it needs companion entries meshio++ does not carry (cyclic, cyclicAMI, processor, mapped*, …): those declare keys like neighbourPatch or myProcNo, and OpenFOAM refuses to load a case whose patch declares such a type without them. A downgraded case loads and solves with boundary conditions you can see and fix. An unknown type is written as patch, never wall — wall selects wall functions, so guessing it would silently change a solve's physics.
  • A boundary cell coinciding with an internal face is dropped with a warning. OpenFOAM cannot put a face shared by two cells on a patch, so writing it would produce a case that does not load.
  • Degenerate volume cells that match a named type's (n_faces, n_points) signature but whose topology doesn't resolve cleanly (_match_top finds more or less than one vertical neighbour per base node) are silently skipped and logged as a warning count, rather than falling back to a general polyhedron.
  • Boundary patches are tagged by patch index, not patch identity across face-size groups — if one named patch contributes both triangles and quads, its triangle CellBlock and quad CellBlock get the same cell_tags id (assigned once per patch, reused across whichever size-buckets that patch's faces fall into), but two different named patches always get distinct ids.
  • The polyMesh lists must agree. The C++ reader checks, before building cells, that every face names a point that exists, that the owner and neighbour lists are no longer than the face list, that owners are non-negative and neighbours at least -1 (the padded form), that no cell id exceeds what the two lists can account for, and that each patch's startFace and nFaces stay inside the faces; a mesh that fails is a ReadError naming the face or patch. Binary lists and field lists must lie inside their files, and an ASCII field whose numbers do not match its declaration is skipped with a warning. A points line that gives fewer than three numbers is still skipped silently (it then fails the face check if a face names the missing point).
  • All binary reads assume little-endian (LSB) — the format's own arch string is trusted for label/scalar width; a declared BSB (big-endian) arch is refused by name (v15.5.0, roadmap §1.1) rather than silently misread, since neither engine byte-swaps.
  • Read goes through the C++ core (meshioplusplus._core.openfoam_read, using std::filesystem for the polyMesh directory), with the Python reference as an automatic fallback. General polyhedra cross the C++↔Python boundary via the ragged polyhedron<N> cell representation (a copied list of face arrays); boundary patch names travel through an OpenFoamInfo side-channel struct as mesh.cell_tags. On WASM, readMeshSelective(path, {format: 'openfoam', info: true}).info.patches reshapes the same OpenFoamInfo as [{familyId, names, type?}]; writeMesh(path, mesh, 'openfoam', {info}) writes it back. See doc/wasm.md's "Side channel (info)" section.

The ordering contract (write) ​

A polyMesh is not just a face list: OpenFOAM requires an ordering, and breaking it produces a mesh checkMesh rejects rather than an error at write time. All of it is enforced and then re-validated before any file is opened, with one check per clause naming the clause it broke. The validator runs in release builds too — release is where large cases get written, and its failure means a corrupt mesh was about to reach a solver.

  1. neighbour holds only internal faces. (meshio++'s own reader also accepts a -1-padded full-length list; OpenFOAM does not — which is exactly why round-tripping through our reader is a weak oracle for this writer.)
  2. owner[i] < neighbour[i] for every internal face.
  3. Internal faces occupy [0, nInternalFaces); boundary faces follow.
  4. Internal faces are sorted by (owner, neighbour) — strictly upper-triangular.
  5. Each internal face's normal points from its owner toward its neighbour; each boundary face's points out of the domain.
  6. Patches partition the boundary range exactly, and every face in a patch's range really belongs to that patch.

Winding is repaired, not required: an inverted (negative-Jacobian) cell is written correctly oriented rather than rejected, because detail/cell_faces.hpp's tables are outward on the reference element and would otherwise emit inward normals for every one of its faces.

Patches on write ​

Patch names and types are recovered from mesh.cell_tags' negative family ids together with mesh.openfoam_patch_types — both of which read sets, so an OpenFOAM → OpenFOAM round trip preserves the boundary file's patch order.

A mesh carrying neither, which is anything converted from another format, gets a single patch named defaultFaces of type patch — OpenFOAM's own name for exactly this, and what blockMesh produces. Patches are never synthesized from geometry (connected boundary components, cell-block membership, …): that would invent structure the mesh never stated and make the patch list depend on the mesh's shape.

Notes ​

  • No tests/python/meshes/ reference fixture (no case directory is checked in); tests/python/test_openfoam.py builds small ASCII/binary polyMesh file sets inline under tmp_path, covering ASCII and binary variants, all 4 named volume cell types, general polyhedra, boundary polygon grouping, and the .foam/case-dir/polyMesh-dir path-resolution forms. Most tests import the internal Python functions directly (keeping the Python reference exercised); the public-API test drives the C++ path.
  • Ported from Simvia's meshlane fork (see CHANGELOG.md) — this format did not exist upstream before that.

Engine parity ​

The case writer, and zone, field and multi-region reading, exist in the C++ core only; the Python reader covers the single-region polyMesh. This is a recorded exemption (roadmap 3.4.1, ENGINE_EXEMPT / PARTIAL_ENGINE_EXEMPT in tests/python/conformance_spec.py), not an open gap: the case layout is implemented once, and _core ships in every wheel.

Released under the MIT License.