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 name | openfoam |
| 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.
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 neededmeshioplusplus.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 aformat ...;line and anarch "...";line, extractinglabel=/scalar=byte widths; defaults to ascii/8-byte if absent. A non-LSBarch(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 byN*3*scalar_bytesraw floats, no per-row framing — read via a singlenp.frombuffer. - Binary
owner/neighbour(labelList):N (followed byN*label_bytesraw ints, same direct-buffer read. - Binary
faces(faceList, non-contiguous): each face is its ownlabelList—<count> ( <count*label_bytes bytes> )repeatedNtimes, notCompactListList/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, sincefind(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 alist[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-zonename { type ...; <key> List<label> N(...); }dict structure as ASCII, with theList<label>payload as raw bytes. The dict structure around it (names, braces, thetype/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 — seeparse_zone_file_binary's own doc comment informats/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 ofpatch_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 viaargsort+bincount) fromowner/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 generalpolyhedron(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:0for 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 (notcell_data),{family_id: [patch_name]}, letting a MED write bridge these patch names through the same mechanism used for Gmsh physical groups (seemed.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_datacome from a selected time directory's field files (U,p,T, …) when requested viatime_step/arrays— see "Time-directory fields" below; the plainread()(notime_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.
mesh = meshioplusplus.read("case.foam")
for r in mesh.regions:
print(r.name, r.kind) # e.g. "porousZone" "cell", "inlet" "side"cellZones→ oneRegionof kind"cell"per zone, entries the zone's cells as global (block-major) indices.pointZones→ oneRegionof kind"point"per zone, entries the zone's point indices, unchanged (points are never reordered by this reader/writer).faceZones→ oneRegionof kind"side"per zone; a face id becomes(global cell, local facet)via its owner cell (the same cell OpenFOAM's ownownerarray names) —flipMapis never read or written, since aSideentry 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
faceZonesentry 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'sZonesRoundTripAsNamedRegionsasserts exactly this invariant;tests/python/test_region_roundtrip.py'sopenfoamrow documents it as the one exception to that table's usual exact-entries assertion. - Boundary patches (
cell_tags/mesh.openfoam_patch_types, theboundaryfile) are a separate, older mechanism and are not regions — a patch is not afaceZone, and this tier does not change that. - The writer only produces
cellZones/faceZones/pointZonesonce the mesh carries aRegionof 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 toneighbour/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=:
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):
mesh = meshioplusplus.openfoam.read("case.foam") # transparent -- no extra argumentEach 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:
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 fieldsvolScalarField/volVectorField/volSymmTensorField/volTensorField(1/3/6/9 components) becomecell_data;pointScalarField/pointVectorField(1/3 components) becomepoint_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 samearch-driven reader the polyMesh files use) are supported.- A cell field's
internalFieldonly ever covers volume cells, in OpenFOAM's own numbering — the matchingcell_datablocks are the volume/polyhedron ones; the boundary-face blocks get thevalueof each patch'sboundaryFieldentry (uniform,nonuniform List<T> N(...)inline or one per line,N{value}, ASCII or binary), matched to the patch by its exact name, andNaNwhere 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;
0is included only when it actually holds fields.time_stepis 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:
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(defaultFalse) selects the encoding;label_bits/scalar_bits(32 or 64, default 32/64 — OpenFOAM's own default build) are only meaningful together withbinary=True. Both are Python and C++ only (OpenFoamWriteOptions) — the registry's generic--binary/--ascii(both CLIs,mio_write_ex, WASM'swriteMesh(..., {encoding})) always use the 32/64 default; changing the width needs the Python or C++ API directly.points/owner/neighbourand zoneList<label>payloads are raw little-endian bytes (count,(, bytes,));facesis the same length-prefixed-per-face shape as ASCII, in binary — see "Binaryfaces" above.boundarystays plain text regardless ofbinary(a handful of small per-patch scalars are never worth binary-encoding, the same reasoninguniformfield values use).- The header's
format/archlines record the encoding and widths (arch "LSB;label=32;scalar=64";), exactly what the reader'sdetect_formatneeds to parse the file back; aboundaryfile (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) forpoints/owner/neighbourat the default width; foamlib has nofaceCompactList/per-face-list reader of its own, sofacesis 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.
writeresolves its path exactly asreaddoes and creates<case>/constant/polyMesh/as needed; a.foamtarget 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 explicitfile_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, throughresolve_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_writeneeds 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
typeis downgraded when it needs companion entries meshio++ does not carry (cyclic,cyclicAMI,processor,mapped*, …): those declare keys likeneighbourPatchormyProcNo, 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 aspatch, neverwall—wallselects 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_topfinds 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
CellBlockand quadCellBlockget the samecell_tagsid (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
startFaceandnFacesstay inside the faces; a mesh that fails is aReadErrornaming 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 ownarchstring is trusted for label/scalar width; a declaredBSB(big-endian)archis 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, usingstd::filesystemfor the polyMesh directory), with the Python reference as an automatic fallback. General polyhedra cross the C++↔Python boundary via the raggedpolyhedron<N>cell representation (a copied list of face arrays); boundary patch names travel through anOpenFoamInfoside-channel struct asmesh.cell_tags. On WASM,readMeshSelective(path, {format: 'openfoam', info: true}).info.patchesreshapes the sameOpenFoamInfoas[{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.
neighbourholds 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.)owner[i] < neighbour[i]for every internal face.- Internal faces occupy
[0, nInternalFaces); boundary faces follow. - Internal faces are sorted by
(owner, neighbour)— strictly upper-triangular. - Each internal face's normal points from its owner toward its neighbour; each boundary face's points out of the domain.
- 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.pybuilds small ASCII/binarypolyMeshfile sets inline undertmp_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.