C++ mesh backends
The C++ core has three interchangeable in-memory mesh backends, selected at build time with the CMake option MESHIOPLUSPLUS_MESH_BACKEND (exactly one is compiled per build, like the parallel backend):
| Backend | Structure | Use it for |
|---|---|---|
MESHIO (default) | Mesh/CellBlock over dtype-erased NDArrays, mirroring the Python meshio.Mesh | The Python extension (required for it — PyPI wheels always use MESHIO) |
NATIVE | Canonical statically-typed storage: Float64 points, Int64 connectivity, CellType enum, CSR ragged blocks | The fastest pure-C++ consumer surface; the WebAssembly build uses it |
KRATOS | A Kratos Multiphysics-style ModelPart (Nodes / Elements / Conditions / SubModelParts) | Near-costless exchange with Kratos (or CoSimIO) via the header-only bridge |
# standalone C++ build with a non-default backend (implies no Python extension)
./build/configure.sh --mesh-backend NATIVE --tests --build
./build/configure.sh --mesh-backend KRATOS --tests --buildEvery format reader/writer is written against a uniform mesh API (cpp/include/meshioplusplus/mesh_api.hpp), so all ~36 formats compile and round-trip identically under every backend — the full GoogleTest suite runs per backend in CI. Selecting NATIVE/KRATOS together with MESHIOPLUSPLUS_BUILD_PYTHON=ON is a CMake configure error: the zero-copy numpy boundary is written against MESHIO's exact struct layout.
The uniform mesh API
meshioplusplus::Mesh is a compile-time alias (cpp/include/meshioplusplus/mesh.hpp) for the selected backend type. All backends implement:
Ingestion (what readers use — NDArray is the universal staging type, handed over by move):
mesh.AssignPoints(NDArray points); // (n, dim), float dtype
mesh.AddCellBlock("tetra", NDArray conn); // (n, npc), integer dtype
mesh.AddPolygonBlock("polygon", rows); // 1-level ragged
mesh.AddPolyhedronBlock("polyhedron", cells); // 2-level ragged
mesh.AddPointData("temperature", NDArray data);
mesh.AddCellData("gmsh:physical", std::vector<NDArray> perBlock);
mesh.AppendCellData("medit:ref", NDArray oneBlock); // incremental variant
mesh.AddFieldData("group", NDArray data);Accessors (what writers use):
mesh.NumPoints(); mesh.PointDim(); mesh.Points(); // const NDArray&
mesh.NumCellBlocks();
for (const auto cb : mesh.CellRange()) { // Mesh::CellView values
cb.Type(); // "tetra", ...
cb.NumCells(); cb.NodesPerCell(); cb.Conn(); // const NDArray&
cb.IsRagged(); cb.Row(i); cb.Face(i, f); // ragged access
}
mesh.PointDataNames(); // always sorted
mesh.PointData("temperature"); // const NDArray&
mesh.CellData("gmsh:physical", blockIndex);Dtype rules: MESHIO stores arrays exactly as received; NATIVE and KRATOS canonicalize within kind (floats → Float64, ints → Int64 — an integer tag array never becomes float, so "first integer cell_data is the tag" format conventions survive). Owning arrays that are already canonical are moved, not copied, and readers produce canonical dtypes almost everywhere, so ingest is near-free. One observable consequence: under NATIVE/KRATOS, a file with Float32 points is re-written as Float64.
The NATIVE backend
meshioplusplus::NativeMesh (backends/native_mesh.hpp) adds a fast-consumer surface on top of the uniform API:
const double* xyz = mesh.PointsData(); // contiguous Float64
std::span<const std::int64_t> conn = mesh.ConnSpan(0); // per-block Int64
meshioplusplus::CellType t = mesh.BlockType(0); // enum, not string
const auto& csr = mesh.GlobalConnectivity(); // whole-mesh CSR
// csr.mOffsets (ncells+1), csr.mConn (flat), csr.mTypes (one per cell)Ragged blocks are stored CSR-style (flat node buffer + offset arrays) rather than nested vectors. GlobalConnectivity() is built lazily and cached.
The KRATOS backend
meshioplusplus::KratosMesh (backends/kratos_mesh.hpp) puts a Kratos-style ModelPart behind the same API:
meshioplusplus::Mesh mesh = meshioplusplus::read_gmsh("part.msh");
meshioplusplus::ModelPart& mp = mesh.GetModelPart(); // materialized lazily
mp.NumberOfNodes(); // Ids are 1-based (node Id = point index + 1)
mp.NumberOfElements(); // cell blocks of the mesh's max topological dim
mp.NumberOfConditions(); // lower-dimension blocks
mp.GetSubModelPart("gmsh_physical_1"); // built from integer tag arrays- Elements vs Conditions: blocks whose topological dimension equals the mesh's maximum become Elements, lower-dimension blocks Conditions (the Kratos convention, matching the mdpa reader/writer), each kind Id-numbered 1..N in block order with default Kratos names (
Element3D4N,SurfaceCondition3D3N, ... —backends/kratos_names.hpp). - Tags → SubModelParts: integer cell-data under well-known names (
gmsh:physical,su2:tag,medit:ref,cell_tags, ...) automatically become SubModelParts named<key>_<value>containing the tagged entities and their nodes. Disable withmesh.SetBuildSubModelPartsFromTags(false)before the firstGetModelPart()call. The tag arrays also stay as elemental/conditional data, so writer round-trips are byte-identical. - point/cell data become simplified per-entity variables (
mp.GetNodalData("temperature"),mp.GetElementalData(...),mp.GetNodalValue("temperature", nodeId)). - Lazy and write-transparent: a plain read → write conversion never builds the ModelPart at all; writer accessors serve from the canonical staging storage, so output matches the NATIVE backend byte-for-byte.
- Mutation: after changing the ModelPart directly (
CreateNewNode, ...), callmesh.InvalidateBlocks(); the block view is then rebuilt from the ModelPart (consecutive same-type Elements group into blocks, then Conditions). Ragged pass-through blocks (polygon/polyhedron — Kratos has no such geometry) and SubModelPart structure are not representable back and are dropped by that rebuild.
The Kratos bridge (works from any backend)
cpp/include/meshioplusplus/kratos_bridge.hpp is header-only, templated, and independent of the selected mesh backend — meshioplusplus::ModelPart and the bridge compile in every build. to_model_part populates any Kratos-like class through the narrow creation API only (CreateNewNode/CreateNewElement(name, id, nodeIds, properties)/...), so it works with a real Kratos::ModelPart without meshio++ ever linking Kratos:
#include "meshioplusplus/kratos_bridge.hpp"
// Real Kratos: map properties ids to Properties::Pointer.
Kratos::ModelPart& dest = model.CreateModelPart("FromMeshio");
meshioplusplus::to_model_part(mesh.GetModelPart(), dest, [&](auto pid) {
return dest.HasProperties(pid) ? dest.pGetProperties(pid)
: dest.CreateNewProperties(pid);
});
// And back (duck-typed via bridge_traits; specialize it for classes whose
// accessors differ from meshioplusplus::ModelPart's shape):
meshioplusplus::ModelPart mine = meshioplusplus::from_model_part(source);Sub model parts (including nested ones) are copied when the destination supports CreateSubModelPart/AddNodes/AddElements/AddConditions. CoSimIO's ModelPart (whose CreateNewElement takes an ElementType enum) is populated with a thin loop instead — CI compile-checks that pattern against the real CoSimIO headers on every run. The conversion cost is one O(n) bulk-create pass — the same cost Kratos's own CoSimIO conversion utilities pay, because Kratos's pointer-based entity storage cannot be aliased from outside.
Benchmarks between backends
benchmark/bench_backends.sh builds one benchmark binary per backend (cpp/benchmark/bench_backends.cpp, CMake option MESHIOPLUSPLUS_BUILD_BENCHMARKS=ON) and collates a CSV comparing ingest, accessor traversal, ModelPart materialization (KRATOS only), and full file round-trips on a synthetic tet cube. See Benchmarks for results and method.
Adding a backend
One CMake branch defining MESHIOPLUSPLUS_MESH_BACKEND_<NAME>, one #elif in cpp/include/meshioplusplus/mesh.hpp, and a backends/<name>_mesh.hpp implementing the uniform API (mesh_api.hpp documents the exact contract; cpp/tests/test_mesh_api.cpp is its executable form and must pass).