# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this project is

meshio++ (Python package/import name `meshioplusplus`) is a Python library for reading and writing many mesh file formats used in scientific computing and FEM (Finite Element Method). It provides a unified `Mesh` data structure that all format readers/writers convert to and from.

This branch (`port-to-c++`) adds a **C++ core** (`meshioplusplus._core`, pybind11) built with **scikit-build-core + CMake**, replacing the old setuptools build. Most formats read/write through the C++ core with a pure-Python fallback.

## Keeping docs in sync (required)

**Every time a feature or format is added or changed, update all three of `CLAUDE.md`, `README.md`, and the `doc/` site in the same change.** At minimum: the format table and format-specific notes in [`doc/formats.md`](doc/formats.md) (plus any other relevant `doc/` page), the supported-formats list in `README.md`, and the architecture/"Adding a new format" guidance here. Changes to the shared format registry or the flat-binding surfaces must also be reflected in [`doc/wasm.md`](doc/wasm.md), [`doc/c_api.md`](doc/c_api.md), and [`doc/fortran.md`](doc/fortran.md). A feature is not "done" until CLAUDE.md, README.md, and the docs all reflect it.

## Commands

**Build/install for development** (native paths on). PEP 668 systems: use the in-repo uv venv (`uv venv --python 3.12 .venv`), then:
```bash
CMAKE_ARGS="-DMESHIOPLUSPLUS_WITH_HDF5=ON -DMESHIOPLUSPLUS_WITH_NETCDF=ON -DMESHIOPLUSPLUS_WITH_ZLIB=ON" \
  uv pip install --python .venv --no-build-isolation -e .
```
Plain environments: `pip install -e ".[all]"`. The optional deps (HDF5, netCDF, zlib) are auto-detected; when absent the C++ paths compile out and Python fallbacks (h5py/netCDF4/stdlib zlib) take over.

**Standalone C++ build**: `build/configure.sh` (Linux/macOS) and `build/configure.bat` (Windows) configure a CMake tree next to themselves (`build/cpp-<type>[-<mesh-backend>]`); flags: `--backend SEQ|STL|OPENMP|TBB`, `--mesh-backend MESHIO|NATIVE|KRATOS` (non-MESHIO implies `-DMESHIOPLUSPLUS_BUILD_PYTHON=OFF`), `--tests`, `--build`, `--with/-out-hdf5/netcdf/zlib`, `--tbb-dir <path>`. They print the matching `CMAKE_ARGS … pip install` line (MESHIO only).

**WebAssembly build** (`@meshioplusplus/wasm` npm package): `build/configure-wasm.sh --build` (needs the [Emscripten SDK](https://emscripten.org/docs/getting_started/downloads.html) on `PATH`) configures `build/wasm-<type>` with `-DMESHIOPLUSPLUS_BUILD_PYTHON=OFF -DMESHIOPLUSPLUS_BUILD_WASM=ON -DMESHIOPLUSPLUS_PARALLEL_BACKEND=SEQ -DMESHIOPLUSPLUS_MESH_BACKEND=NATIVE` (the fastest mesh backend; the JS API shape is unchanged and `meshBackend()` reports `"native"`), HDF5/netCDF off, builds `bindings_js/js_bindings.cpp` (embind) against the same `meshioplusplus_core_obj` as `_core`, and copies `meshioplusplus_wasm.{mjs,wasm}` into `wasm/dist/`. Ships 31 formats + XDMF's XML/Binary path — 32 readable, 31 writable (`openfoam` read-only), no HDF5/netCDF-backed formats — see `doc/wasm.md`. Smoke test: `node wasm/test/smoke.mjs`. `-fwasm-exceptions` is required (applied `PUBLIC` on `meshioplusplus_core_obj` under `if(EMSCRIPTEN)`) so `ReadError`/`WriteError` surface as catchable JS `Error`s instead of aborting; `log.hpp`'s `std::osyncstream` usage has a `std::mutex`-guarded fallback for standard libraries (Emscripten's non-threaded libc++) that ship `<syncstream>` without actually defining it (`__cpp_lib_syncbuf` unset). `configure-wasm.sh` runs `embuilder build zlib` right after configuring (when zlib is on) to pre-warm Emscripten's zlib port cache single-threaded — `-sUSE_ZLIB=1` makes every translation unit trigger that port's build on first use, and a parallel `-j` build races many `em++` invocations against the same cold-cache lock, aborting with "attempt to lock the cache while a parent process is holding the lock (sanity)". CI (`.github/workflows/wasm.yml`) builds+smoke-tests and publishes to npm (`NPM_TOKEN` secret) only on `v*` tags (mirrors `wheels.yml`'s trigger), plus `workflow_dispatch`.

**C API / Fortran build** (`libmeshioplusplus` + Fortran module, for C/Fortran HPC consumers): `build/configure.sh --c-api` or `--fortran` (implies `--c-api`; both also exist in `configure.bat`, Fortran untested on MSVC) → CMake options `MESHIOPLUSPLUS_BUILD_C_API` / `MESHIOPLUSPLUS_BUILD_FORTRAN` (both OFF by default). `cmake --install <tree> --prefix <p>` installs the shared lib(s), the single pure-C header `bindings_c/include/meshioplusplus/meshioplusplus.h`, a `find_package(meshioplusplus)` config package, a relocatable pkg-config file, and (Fortran) the `.mod` **plus** `bindings_fortran/meshioplusplus.f90` (`.mod` files are compiler-specific; consumers on another compiler recompile the module — the HDF5 approach). Tests: `cpp/tests/test_c_api.cpp` (auto-globbed into the gtest suite when C_API is on) and the `fortran_api` ctest (`bindings_fortran/test/test_fortran_api.f90`). CI-tested consumer examples: `doc/examples/c_api_example.c`, `doc/examples/fortran_example.f90`. **`project()`'s `VERSION` must be bumped together with pyproject.toml's `version`** — it feeds `mio_version()`, the `.so` VERSION/SOVERSION, and the package configs.

**Run tests:** `pytest tests/` (or `.venv/bin/python -m pytest tests/`).
Single file/test: `pytest tests/test_gmsh.py::test_gmsh22`.

**Lint / format:** `pre-commit run -a` (isort, black, flake8). Or `black --check . && flake8 . && isort --check .`.

**CLI usage:**
```bash
meshioplusplus convert input.msh output.vtk
meshioplusplus info mesh.msh
meshioplusplus ascii mesh.msh         # convert to ASCII
meshioplusplus binary mesh.msh        # convert to binary
```

**Docs (VitePress):** `cd doc && npm install && npm run docs:build` (dev: `npm run docs:dev`).

**Logo:** built with TikZ. `logo/build.sh` runs `logo/gen_logo_tikz.py`
(numpy+matplotlib triangulation of an "FE blob" → `logo/_mesh_icon.tex`),
compiles `logo/logo.tex`/`logo-icon.tex` with `pdflatex`, and converts to SVG
via `dvisvgm` (PNG via PyMuPDF). Committed assets: `logo/logo-with-text.svg`
(README banner + `doc/public/logo.svg`), `logo/logo-icon.svg` (favicon/nav).
The old pygmsh generator `logo/logo.py` is superseded (see `logo/README.md`).

**Example notebooks** (`example/*.ipynb`): read the bundled `example/example.msh`
with meshio++ and render/convert it (PyVista off-screen via a VTU round-trip,
matplotlib fallback). Committed **with outputs**; re-execute with
`PYVISTA_OFF_SCREEN=true jupyter nbconvert --to notebook --execute --inplace example/*.ipynb`.

**Benchmarks** (`benchmark/`): `bench.py` times read/write on formats both
libraries support — meshio++ vs the legacy pure-Python `meshio` (imported from
`/home/vicente/src/meshio_legacy/src` via `sys.path`, no build). `inputs.py`
provides the real `example/example.msh` bracket (the headline input) + a
synthetic numpy tet grid (also used for a size-scaling sweep). `01_benchmark.ipynb`
runs it, writes `results.csv`, and regenerates the plots in
`doc/public/benchmarks/` (`benchmark_times`/`_speedup` = the bracket,
`benchmark_scaling` = speedup vs mesh size) shown on `doc/benchmarks.md`. The needed extras
(`pyvista matplotlib jupyter nbconvert ipykernel`) are installed into `.venv`
with `uv pip install`. `bench_hotpath.py` is a standalone (no-legacy) micro-bench
for the C++ container hot paths — it round-trips a shared-vertex triangle grid
through WKT and times the reader's per-vertex dedup; A/B a `main` build vs a branch
by comparing its `min` (WKT read is parse-bound, so container-level effects sit near
the machine's jitter floor). `bench_backends.sh` + `cpp/benchmark/bench_backends.cpp`
(built with `-DMESHIOPLUSPLUS_BUILD_BENCHMARKS=ON`) compare the **mesh backends**:
one binary per backend (the backend is compile-time), CSV collated into
`benchmark/results_backends.csv` — ingest / accessor traversal / `to_modelpart`
(KRATOS materialization) / per-format file round-trips on a synthetic tet cube;
results table in `doc/benchmarks.md`.

## Architecture

**Core data model** (`src/meshioplusplus/_mesh.py`, pure Python, unchanged):
- `Mesh`: holds `points`, `cells` (list of `CellBlock`), `point_data`, `cell_data`, `field_data`, `point_sets`, `cell_sets`, optionally `gmsh_periodic`/`info`
- `CellBlock`: a cell type string (e.g. `"triangle"`, `"tetra10"`) + numpy node-index array
- `_common.py`: `num_nodes_per_cell`; `topological_dimension` in `_mesh.py`

**C++ core** (`cpp/`, `bindings/`):
- `cpp/include/meshioplusplus/` headers (`ndarray.hpp`, `mesh.hpp`, `mesh_api.hpp`, `cell_type.hpp`, `types.hpp`, `registry.hpp`, `backends/*`, `detail/*`, `formats/*`), `cpp/src/formats/*.cpp`, compiled with `bindings/_core.cpp` into `meshioplusplus._core`.
- **Format dispatch registry** (`registry.hpp` + `cpp/src/registry.cpp`, compiled into `meshioplusplus_core_obj`): the C++-level `format name -> read/write function` tables, `extension -> default format` map, `resolve_format()`, and `registry_compiled_out()` (names the missing optional dep for HDF5/netCDF formats absent from a build; their extensions are mapped unconditionally so errors say "no HDF5 support" instead of "cannot infer format"). Shared by the **flat bindings** — WASM (`bindings_js/js_bindings.cpp`, which no longer carries its own tables) and the C API (`bindings_c/`) — while the pybind11 binding keeps its one-function-per-format surface with dispatch in Python (`_helpers.py`). Parameterized writers get fixed defaults here (documented per entry; matching the Python reference defaults, except xdmf which follows the build: HDF when HDF5 is available, XML otherwise). **A new C++ format must also be registered here** (reader/writer entry + extension default) or the flat bindings won't see it.
- **C API** (`bindings_c/`): `include/meshioplusplus/meshioplusplus.h` (the single installed pure-C99 header; the C++ headers make no ABI promise) + `c_api.cpp`, built into the installable `libmeshioplusplus` shared lib (SOVERSION 0) when `MESHIOPLUSPLUS_BUILD_C_API=ON`. Flat whole-mesh API over the **uniform mesh API only**, so it compiles under all three mesh backends and the gtest C-API suite runs per-backend in CI. Contract: `mio_status` codes + thread-local `mio_last_error()` (no exception crosses the ABI — every extern "C" body runs in `guarded()`/`guarded_ptr()`); setters copy, getters are zero-copy borrows valid until the next mutating call; strings via caller-buffer + required-length return (never `c_str()` of a per-backend-possibly-temporary `Type()`); row-major arrays, 0-based connectivity (int32 widened to int64 on ingest); ragged blocks are reported (`is_ragged`) but their connectivity is inaccessible (v1); side-channel structs (point_sets/cell_sets/MED families) are dropped like WASM. The `mio_cell_type` enum duplicates the cell-type list in the C header and `c_api.cpp` static_asserts every entry against `CellType`, so drift is a compile error.
- **Fortran module** (`bindings_fortran/meshioplusplus.f90`, module `meshioplusplus`, `MESHIOPLUSPLUS_BUILD_FORTRAN=ON` which force-enables the C API): modern OO Fortran 2008 (`type(mio_mesh)` with type-bound procedures, HDF5/PETSc style) over the C API via ISO_C_BINDING; raw `bind(c)` interfaces are private (`c_mio_*`). Conventions: arrays are Fortran-shaped — points `(dim, n)`, conn `(npc, ncells)` — which is the *same memory* as the C row-major shapes (no transpose anywhere); connectivity is 1-based, shifted ±1 inside the copying setters/getters only; `points_ptr` is the zero-copy borrow (no shift needed); every fallible procedure takes optional `stat`/`errmsg`, absent `stat` + failure = print + `error stop` (stdlib pattern); handles freed explicitly (`m%free()`, no finalizer).
- **Mesh backends** (`mesh.hpp` dispatch; mirrors the parallel-backend pattern): `MESHIOPLUSPLUS_MESH_BACKEND` = `MESHIO|NATIVE|KRATOS` selects, at compile time, which structure `meshioplusplus::Mesh` is — exactly one `MESHIOPLUSPLUS_MESH_BACKEND_*` macro is defined. `MESHIO` (default, `backends/meshio_mesh.hpp`) is the meshio-mirroring `Mesh`/`CellBlock` and is **required** when `MESHIOPLUSPLUS_BUILD_PYTHON=ON` (CMake `FATAL_ERROR` otherwise; `np_conversions.hpp` `#error`s too) — PyPI/wheels are unaffected by the other backends. `NATIVE` (`backends/native_mesh.hpp`) is canonical statically-typed storage (Float64 points, Int64 connectivity, `CellType` enum from `cell_type.hpp`, CSR ragged blocks, lazy whole-mesh CSR via `GlobalConnectivity()`); the WASM build uses it. `KRATOS` (`backends/kratos_mesh.hpp`) wraps a Kratos-style `ModelPart` (`backends/model_part.hpp`: Nodes/Elements/Conditions with 1-based Ids, nested SubModelParts sharing root-owned entities, simplified per-entity variables) — ingest stages canonically, `GetModelPart()` materializes lazily (Elements = blocks of max topological dimension, Conditions = lower, names from `backends/kratos_names.hpp` ported from `_mdpa.py`; integer tag cell-data like `gmsh:physical` auto-becomes SubModelParts, toggle `SetBuildSubModelPartsFromTags(false)`), writers serve from staging so round-trips never pay for the ModelPart; after direct ModelPart mutation call `InvalidateBlocks()`. `kratos_bridge.hpp` (header-only, backend-independent) `to_model_part`/`from_model_part` populate/read ANY Kratos-like class — incl. real `Kratos::ModelPart` via a properties-getter — with one O(n) bulk-create pass; CI compile-checks it against real CoSimIO headers (fetched at CI time only — CoSimIO's BSD-4-clause-with-advertising license is never vendored into this MIT repo; `model_part.hpp` is a clean-room implementation). `_core.__mesh_backend__` / JS `meshBackend()` report the active backend. **Adding a backend** = one CMake branch defining `MESHIOPLUSPLUS_MESH_BACKEND_<NAME>` + one `#elif` in `mesh.hpp` + a `backends/<name>_mesh.hpp` implementing the uniform API (`cpp/tests/test_mesh_api.cpp` must pass). CI runs the gtest suite once per backend (`ci.yml` `cpp-tests` matrix).
- **Uniform mesh API rule** (`mesh_api.hpp`): format code (`cpp/src/formats/*`, shared `detail/*` helpers, `bindings_js/`) MUST go through the uniform methods — ingest `AssignPoints`/`AddCellBlock`/`AddPolygonBlock`/`AddPolyhedronBlock`/`AddPointData`/`AddCellData`/`AppendCellData`/`AddFieldData`, accessors `Points()`/`PointDim()`/`Cells(i)`/`CellRange()` (`Mesh::CellView` with `Type()`/`NumCells()`/`NodesPerCell()`/`Conn()`/`IsRagged()`/ragged `Row`/`Face`) and `PointDataNames()`/`PointData(name)`/`CellData(name, block)`/`CellDataNumBlocks`/`FieldData(name)` (+ `Has*`/`Num*`) — and never backend struct members; that's what lets one format compile under all three backends. Hoist `Points()`/`Conn()`/data lookups out of per-element hot loops. The one sanctioned exception is `bindings/np_conversions.hpp` (pinned to MESHIO). NATIVE/KRATOS canonicalize dtypes at ingest *within kind* (floats→Float64, ints→Int64, never int→float — "first integer cell_data is the tag" conventions survive); owning canonical arrays are moved, not copied.
- **Zero-copy at the I/O boundary**: readers return capsule-backed writeable numpy; writers view numpy memory (`bindings/np_conversions.hpp`). The conversion layer carries points/cells/point_data/cell_data/field_data, but **not** `mesh.info`, `cell_sets`, or `point_sets` — formats that need those either defer to Python or carry them out-of-band via a **side-channel struct** the binding `setattr`s onto the Python `Mesh` (e.g. `MedInfo`, `AnsysInfo`/`UnvInfo` for `point_sets`/`cell_sets`, `OpenFoamInfo` for `cell_tags`). **Field-only formats** (`mff`/`dex`/`ip`, the Modulef/FLUX/ANSYS-Fluent field files) carry no geometry: they read/write a geometry-less `Mesh` (no cells, field values in `point_data`; `dex`/`ip` also fill `points` from the file's coordinates, `mff` has none so its `points` has zero columns) — there is no companion-mesh auto-pairing.
- **Ragged cell blocks**: every backend supports optional ragged blocks — 1-level (jagged polygons, `AddPolygonBlock`) and 2-level (list of faces per cell, `AddPolyhedronBlock`) — for types that can't fit a rectangular `NDArray`, read back via `CellView`'s `RowSize`/`Row` and `NumFaces`/`Face`. MESHIO stores them as nested vectors (`CellBlock::mPolygonRows`/`mPolyhedronRows`), NATIVE as CSR offset arrays, KRATOS as staging pass-through (no ModelPart entities). Zero-copy applies only to rectangular blocks; ragged blocks are **copied** across the Python boundary. `py_to_mesh`'s `allow_ragged` flag is **off by default** (so every rectangular-only writer keeps safely rejecting ragged meshes → Python fallback) and only the ragged-aware bindings (MED write) opt in.
- **Container convention**: use `std::unordered_map`/`std::unordered_set` for name/id lookup and dedup tables (O(1), order-irrelevant) — the default. Reach for `std::map`/`std::set` **only** when sorted iteration order is itself relied on (see `su2` `tag_counts`, `openfoam` `by_n`, gmsh's `node_data`/`elem_data` staging locals). The mesh data maps' name order **is** observable — it drives Python dict key order and the on-disk field/variable order of writers (VTU, VTK, XDMF, Exodus, Tecplot, HMF, PLY, AVS-UCD, tetgen, h5m) plus "first int field" selectors (medit, ugrid, su2, netgen, avsucd). The uniform API bakes that guarantee in: `PointDataNames()`/`CellDataNames()`/`FieldDataNames()` **always return sorted names** on every backend (the former `detail::sorted_keys` idiom, now behind the accessors; `map_order.hpp` remains for non-Mesh maps and the pybind layer), so output stays byte-identical across backends.
- **C++ naming convention**: Kratos Multiphysics style — classes/structs/enums and methods (member functions) are `PascalCase` (e.g. `Mesh::NumPoints()`, `CellBlock::IsRagged()`); member variables are `m`-prefixed `PascalCase` (e.g. `Mesh::mPoints`, `CellBlock::mData`); reference parameters are `r`-prefixed (`const std::string& rPath`) and pointer parameters `p`-prefixed (`const int* pPerm`) — forwarding references (`F&&`) and rvalue-reference/move parameters are exempt; free functions, local variables, and lambda parameters stay `snake_case` (e.g. `read_vtu`, `parallel_for`, `detail::sorted_keys`). File names stay `snake_case`. Brace style is K&R (opening brace on the same line) at 4-space indentation — codified in the repo-root `.clang-format`, run via `clang-format -i` over `cpp/`, `bindings/`, `bindings_js/` (never `cpp/third_party`). None of this renames the pybind11/embind **string literals** that define the Python/JS-facing API (`.def("...")`, `.attr("...")`, `emscripten::function("...", ...)`) — those stay exactly as they are; only the C++ symbols on the other side of each binding call follow this convention.
- **Optional deps** (`CMakeLists.txt`): `MESHIOPLUSPLUS_WITH_HDF5`/`_NETCDF`/`_ZLIB`/`_EIGEN` options → `MESHIOPLUSPLUS_HAS_*` compile definitions and `_core.__has_hdf5__`/`__has_netcdf__` flags. `#ifdef`-guarded sources become empty TUs (or fall back to a plain loop) when off. Shared HDF5 helpers in `cpp/include/meshioplusplus/detail/hdf5_util.hpp`. **Eigen** is vendored as a git submodule at `cpp/third_party/eigen` (header-only; run `git submodule update --init` for source builds) and used for the MED Fortran↔C transpose (`med.cpp`, guarded by `MESHIOPLUSPLUS_HAS_EIGEN`); when the submodule is absent the code uses a hand-written transpose, so sdists without submodules still build.
- **C++ standard: C++20** (pinned in `CMakeLists.txt` twice: `CMAKE_CXX_STANDARD` + `target_compile_features`). macOS needs deployment target ≥ 13.3 for `std::format` (set in `ci.yml`/`wheels.yml`).
- **Logging** (`cpp/include/meshioplusplus/log.hpp`): `meshioplusplus::log::debug/info/warn/error("fmt {}", …)` — `std::format` (compile-time-checked) + `std::source_location`, thread-safe via `std::osyncstream`, filtered at runtime by the `MESHIOPLUSPLUS_LOG_LEVEL` env var (`debug|info|warn|error|off`, default `warn`). No printf/cerr elsewhere; errors remain exceptions.
- **Parallelism** (`cpp/include/meshioplusplus/parallel.hpp`): `meshioplusplus::parallel_for(n, f, grain=2048, max_threads=0)` with a compile-time backend selected by `MESHIOPLUSPLUS_PARALLEL_BACKEND` = `AUTO|SEQ|STL|OPENMP|TBB`. **Default is `AUTO`**, which prefers **OpenMP** (portable: libgomp on manylinux, MSVC built-in, libomp on macOS; needs no TBB), else the STL(+TBB) probe, else SEQ. `_core.__parallel_backend__` reports the active backend from Python (STL-without-TBB is effectively sequential — that's why AUTO avoids it). Adding a backend (Kokkos, …) = one CMake branch + one `#elif` in `parallel.hpp`. Iterations must be independent; the first exception is captured and rethrown after the join; `n <= grain` runs sequentially. **Two flavors:** `parallel_for` (all cores — for compute-bound loops: zlib/base64 in `detail/vtu_binary.hpp`, ASCII formatting) and **`parallel_for_bw`** (thread-capped, `parallel_bandwidth_threads=4` — for memory-bandwidth-bound loops: byte-swap/transpose/gather, which *regress* past ~4 threads because they saturate memory bandwidth). Use `parallel_for_bw` for byte I/O and index gathers; `parallel_for` only when the per-element work is real compute. The OpenMP backend uses `schedule(dynamic, grain/4)` — on hybrid CPUs (P+E cores) a static split makes E-cores stragglers; the chunk honours explicitly coarse grains (the VTU zlib loop passes `grain=1` so each block dispatches individually — do NOT floor the chunk above the grain). Hoist per-element dtype switches with `detail::dispatch_dtype(dt, []<class T>(){…})` (`value_io.hpp`) before parallelizing. **Binary I/O rule:** never write per-element to a `std::ostream` (byte-at-a-time `os.put`/tiny `os.write` was the pre-optimization VTK/Gmsh bottleneck) — build one pre-sized buffer (fused gather+byteswap where possible) and emit it with a single `os.write`; read binary sections straight into the destination array (or bulk seek+read, never `istreambuf_iterator`). Endianness conversion goes through `detail/byteswap.hpp` (`bswap_copy`/`bswap_inplace`, single-instruction intrinsics) — never a per-byte reversal loop.

**Format registration** (`src/meshioplusplus/_helpers.py`): each format's `__init__.py` calls `register_format(name, extensions, reader, {name: writer})`. `read`/`write` auto-detect from extension. Modules are imported (and thus registered) in `src/meshioplusplus/__init__.py`.

**Format module layout** (the shim pattern, e.g. `src/meshioplusplus/su2/`):
- `__init__.py` — imports `_core`, defines `read`/`write` that **try the C++ function and fall back** to the Python reference on any exception, then `register_format(...)`.
- `_<format>.py` — the pure-Python reference reader/writer.
- Multi-version formats (Gmsh, VTK) dispatch to version submodules.

**Cell type naming**: meshio++ uses its own names (`triangle`, `tetra10`, `hexahedron20`, …). Each format maps between its native names and meshio++'s (e.g. `gmsh/common.py`).

**Tests** (`tests/`): `helpers.py` has `Mesh` fixtures + `write_read()` (the round-trip pattern). Per-format `test_<format>.py` parametrize over meshes. `tests/meshes/` and `tests/input/` hold read-only reference files (Git-LFS). HDF5/netCDF suites use `pytest.importorskip`.

**CLI** (`src/meshioplusplus/_cli/`): `convert`, `info`, `ascii`, `binary`, `compress`, `decompress`, registered in `_main.py`.

**CI** (`.github/workflows/`): `ci.yml` (3-OS Python test matrix — Linux/macOS build native paths, Windows builds them off and uses Python fallbacks — plus a `cpp-tests` matrix running the gtest suite per mesh backend: MESHIO×STL, MESHIO×OPENMP, NATIVE×OPENMP, KRATOS×OPENMP, the non-MESHIO legs with `BUILD_PYTHON=OFF`; every leg also builds with `BUILD_C_API=ON`/`BUILD_FORTRAN=ON` so the C-API gtests and the `fortran_api` ctest run per backend; the NATIVE leg additionally does a `cmake --install` + external-consumer smoke test — gcc via pkg-config, gfortran against the installed `.mod`, and a `find_package` mini-project, compiling `doc/examples/*`; the KRATOS leg also compile-checks `kratos_bridge.hpp` against real CoSimIO headers cloned at CI time), `wheels.yml` (cibuildwheel with native paths off + PyPI trusted publishing on `v*` tags), `docs.yml` (VitePress → GitHub Pages).

## Adding a new format

1. Create `src/meshioplusplus/<format>/_<format>.py` (Python reference `read`/`write`) and `__init__.py` (shim + `register_format`). `register_format` may also live directly in `_<format>.py` for a Python-only format with no shim to wrap (e.g. `mdpa`, `neuroglancer`, `svg`, `ansysInp`, `openfoam`).
2. Add the C++ implementation: `cpp/src/formats/<format>.cpp` + `cpp/include/meshioplusplus/formats/<format>.hpp`, bind it in `bindings/_core.cpp`, register it in the shared dispatch registry (`cpp/src/registry.cpp`: reader/writer entry + extension default — that's what makes it reachable from WASM and the C/Fortran API), and switch `__init__.py` to the try-C++/Python-fallback shim. New `.cpp` files are auto-globbed by CMake. The implementation must build/read the mesh **through the uniform mesh API only** (see the "Uniform mesh API rule" above) so it works under every mesh backend. **This step is optional** — a format can stay Python-only indefinitely (precedent: `mdpa`, `neuroglancer`, `svg`, and the Simvia-contributed `ansysInp`/`openfoam`); don't force a C++ port just to satisfy this checklist.
3. Import the module in `src/meshioplusplus/__init__.py` (both the import tuple and `__all__`).
4. Add `tests/test_<format>.py` using `helpers.write_read()`. Verify a direct C++ roundtrip and cross-compat with the Python reference (if a C++ path exists).
5. **Update `README.md`, `doc/formats.md`, and this file** (see "Keeping docs in sync").

Do not copy test data from GPL sources (e.g. FEconv examples) into this MIT repo — generate reference files via round-trip instead. Code/fixtures from other MIT-licensed forks in the same lineage (e.g. the [meshlane](https://github.com/simvia-tech/meshlane) fork) may be copied directly; credit the source in `CITATION.cff`/`CHANGELOG.md`.

**Note on `med`:** the C++ core (built with HDF5) handles the mesh-representation part of MED exactly — points, point/cell tags, families with `GRO` group names, mesh-level metadata (`mesh_name`/`description`/`unit_time`/`unit_coords`/`point_tag_groups`/`cell_tag_groups`, via `MedInfo`), node-orientation permutations, and `POG`/`POG2` ragged polygons — and `meshioplusplus.med.read`/`write` use it by default. It **raises** (→ Python fallback) for the constructs it does not replicate byte-for-byte: `CHA` fields (MED-4.1 bitmask / units / step metadata), the `gmsh:physical`→family bridging, non-default profiles/ELGA, and multi-mesh files (`read_med_multi`/`write_med_multi` are always Python). The C++ MED reader iterates `MAI` cell blocks in HDF5 **creation order** (`group_links_crt`, matching h5py `track_order`) since block order aligns cell_data/cell_sets; the shim reconstructs `point_sets`/`cell_sets` from families via the shared Python helpers. See [`doc/formats/med.md`](doc/formats/med.md#quirks-limitations).
