meshio++ roadmap
Status at time of writing: v19.0.0, ABI 44. What ships is listed where it is documented rather than counted here, where counts went stale: the formats in supported formats and the conformance matrix, the operations in architecture, the language surfaces in the C API, Fortran, Julia, R, Octave / MATLAB and WebAssembly pages.
This document lists what is not built. Nothing here duplicates shipped functionality; where a feature partially exists, the shipped half is named and the gap is stated explicitly. Release history lives in CHANGELOG.md, not here.
Effort key: S = days, M = a couple of weeks, L = a month or more, XL = a project in its own right. An item names a probe — the failing test that proves the gap — wherever one is cheap.
How this file works
- Sections are ordered, and the order is the recommendation. Each section states what belongs in it; an item that sounds exciting does not move up for that reason. An empty section is removed, not kept as a placeholder.
- A closed item is removed, not struck through. Its history is the
CHANGELOG.mdentry and the feature's owndoc/page; a partly closed item is narrowed to what remains (theAGENTS.mdchange checklist rule). - Every level of the outline is numbered, so a gap can be cited in an issue, a commit or a release note: a group of work is
section.item(a###/####heading, or a bold label line), the work under itsection.item.subitem, and a bullet under thatsection.item.subitem.subsub. Lists that record a decision rather than open work — Considered, not queued, a section's Landing an item steps and the non-goals — stay unnumbered, because a number here means work to do. The numbering follows the sections, so a new section inserted at the top renumbers the rest, as the rule above says, and so does a section removed once its last item closes (Performance, October 2026: what was §4 onward moved up one; and again when reader and writer parity closed, the same month: operations, ecosystem reach and the full WebAssembly build became §3 to §5). The long-run section closed the same month as the last of its spikes landed (findings); being last, it renumbered nothing. Mesh generation (§6) closed the same month, also last: its native items landed, its measured-and-parked notes moved to mesh generation with their numbers and its references to mesh generation's further reading, its decisions to the non-goals, and its remaining dependencies to the sections they wait on (2.3, 3.3.1, 3.4), then 2.3, 3.2.1 and 3.3. The full WebAssembly build (§5) closed the same month, also last: its constrained primitive fitting landed (fit_cad'sconstrain_primitives), what it measured and left of skin to CAD moved to fit CAD's measured and parked notes, its decisions to the non-goals, and MMG's port to the browser to the section it waits on (3.3.8, then 3.2.8). Generation (§3.1) closed the same month: its primitive constructors landed on every surface, with full-Lagrangeorder=2and the VTK Lagrange orders, the hand-rolled test helpers it left behind and why moved to parametric primitives, and its Deliberately not list to the non-goals; what was 3.2 to 3.4 became 3.1 to 3.3. Selection (§3.1) closed the same month, whenselect(3.1.3) landed on every surface beside thefind_intersectionsandthicknessit was listed with; what was 3.2 and 3.3 became 3.1 and 3.2. Volume mesh improvement (§3.2) closed the same month, whenoptimize_volumegained its worst-cell objective (3.2.1: guarded relocation, edge and multi-face removal, worst-cell smoothing), measured on Netgen's output in optimize_volume; being last, it renumbered nothing. The operations section (§3) closed with it, when MMG reached the full WebAssembly artifact (3.1.8: MMG in the full artifact, statically and without ELAS, its size measured there): ecosystem reach became §3, its items 4.1 to 4.7 now 3.1 to 3.7. In November 2026 an audit of the library against its own decisions, its performance and its ParaView plugin turned up two defects in the plugin, which opened Correctness debts as §1, so format reach, quality and ecosystem reach became §2 to §4 (what was 1.2.11 is now 2.2.11); and it appended Performance, the ParaView plugin and New capabilities as §5 to §7, the last taking the operators format reach's Considered, not queued had listed and the decisions recent work reopened. In November 2026 a review of the published feature list of ScanRuler, an AGPL-3.0 scan-inspection and reverse-engineering tool, appended Scan metrology and reverse engineering as §8 (ideas only; non-goals); being last, it renumbered nothing. New capabilities (§7) closed the same month, when its last item, the hexahedra of creased scans (7.9.2), was measured and parked in octree_mesh as a limit of the output after neither a smoothed skin nor a boundary let off it removed the cells, and 7.9.1 turnedmodel_tolerancesandvirtual_faceson in v19.0.0: scan metrology became §7, its items 8.1 to 8.9 now 7.1 to 7.9. In November 2026 the first measure of duplication by place (tools/dup_report.py, while SonarCloud's first analysis was pending) appended Code duplication as §8, and SonarCloud's first analysis, the same day, SonarCloud findings as §9; being last, they renumbered nothing. The shared-utilities pass the same month closed 8.1, 8.5.2, 8.7.3, 8.8.1 to 8.8.6, 8.9.2 and 8.9.4: what was 8.2 to 8.11 became 8.1 to 8.10, their subitems renumbered within them. The binding pass the same month closed 8.1.2 to 8.1.4, 8.2.3, 8.3.1, 8.6.1, 8.8.1 and 8.8.2 and narrowed the rest: 8.3.2 became 8.3.1, 8.6.2 became 8.6 and 8.8.3 became 8.8.1; the sections kept their numbers. The third pass closed the rest but three, which became 8.1 (OpenCASCADE, what was 8.7.1), 8.2 (the subset and clean gather, what was left of 8.8.1) and 8.3 (installed headers, what was 8.9). The SonarCloud pass the same month worked through 9.1 to 9.4 as the first analysis had listed them and left the three things only the next scan settles, now 9.1 to 9.3. Scan metrology (§7) closed the same month, when its last items landed: the profile solids (7.7.4) through a CAD provider and the viewer's measuring mode (7.9.1), after the fits, elements and GD&T, alignment, deviation, thickness, sections to sketches, the reverse-engineering loop, scan editing and the MCP prompts; booleans with fitted solids (7.8.7) went to the non-goals, and its Considered, not queued list stays in the git history with the section. Code duplication and SonarCloud findings became §7 and §8, the latter's items 9.1 to 9.3 now 8.1 to 8.3. Since then a heading other pages link to carries an explicit anchor without its number ({#awaiting-a-licensed-run},{#quality-of-implementation}), so a renumbering no longer breaks those links: link to the anchor, and cite the number in the text. The ParaView pass the same month closed Correctness debts (§1) and the ParaView plugin (§6): its payload layer closed 1.2, and 1.1 turned out to be backwards, the permutation meshio++ applied to linear wedges on the way to VTK being the defect (removed library-wide, ABI 44); the plugin's items 6.1 to 6.11 landed, 6.3.2 as a library change to the VTK writers. Format reach, quality, ecosystem reach and performance became §1 to §4 (what was 2.2.11 is now 1.2.11, 3.4 is 2.4, 4.1 is 3.1, 5.1 is 4.1), code duplication and SonarCloud findings §5 and §6 (8.1 to 8.3 now 6.1 to 6.3). A review of the whole library the same month, of its formats and operations against what its own pages say they leave out, of its tooling against what the workflows and the build actually run, and of the non-goals against what has shipped since each was decided, extended the sections in place (1.3 and 1.4, 2.6 to 2.17, 3.11 to 3.15, 4.12 to 4.15) and appended Data model as §7 and Operations as §8; being last, they renumbered nothing. It reopened five decisions, each narrowed where it is recorded and queued under the number named there, corrected the item numbers earlier renumberings had left behind in cross-references (which the test now listed as 3.16.2 would have caught), and marked every item it estimated from adoc/page alone verify first. A second pass the same month put code anchors on every open item, so that taking one up needs no planning pass (landing checklists), and corrected against the code what the first had written from the pages: among others that CGNS polyhedra need no cgnslib, that the THREADS backend shipped in v19.0.0, that the R package cannot build on CRAN as it stands, and that the MMG and Netgen add-on wheels carry no LGPL text. It found that Ansys.rstrecords flagged both sparse forms (then 1.1.2) are decoded silently as one of them, which reopened Correctness debts as §1: format reach, quality, ecosystem reach, performance, code duplication, SonarCloud findings, data model and operations became §2 to §9, every item keeping its place within its section (what was 2.6 is 3.6, 8.1 is 9.1), except that the two remaining verification items of 1.1 became 2.1.1 and 2.1.2 and the chatbot's four parts became 4.4.1 to 4.4.5. The first slice of format reach closed the same month: the cgnslib reader reads every zone (what was 2.3.1.1) and OpenFOAM's compressed cases are read and written (what was 2.3.5.1), so 2.3.1.2 to 2.3.1.7 became 2.3.1.1 to 2.3.1.6 and 2.3.5.2 and 2.3.5.3 became 2.3.5.1 and 2.3.5.2, with 2.3.5.3 the compressed write's remaining surfaces. - Defect-shaped items go in a Correctness debts section at the top, regardless of size — behaviour that loses data, mis-orients cells, does not terminate on valid input or fails silently is not a feature request, even when the fix and the feature are the same work. The section is removed when its last item closes, which renumbers the rest, and the next defect found opens it again as §1.
- An item estimated from a doc, a
.d.tsor a changelog alone says so ("verify first") and names its probe; the code has repeatedly been more or less capable than its description. - Non-goals record decisions already taken, with their reasons, so they are not re-proposed as gaps.
- Every open item is written to be picked up without a planning pass (November 2026). It states what exists today with the place in the code that proves the gap, what to reuse, ordered steps numbered so one can be closed alone, tests and fixtures, and a done when probe; a design-pass item lists the decisions to take and the option the code favours. Line numbers are those of
masterin November 2026 and drift: each is given beside a function, option or message name, which is what to search for. A claim an item could not check in the code says verify first.
Landing checklists
Two lists the items cite instead of repeating. They are what the most recent format (dxf) and operation (silhouette, fit_curves) commits touched; AGENTS.md holds the rules behind them. Sources and tests are globbed by CMakeLists.txt, so neither needs a CMake edit.
Checklist F: a format, or a new direction or construct of one.
- Core:
src/cpp/include/meshioplusplus/formats/<fmt>.hppandsrc/cpp/src/formats/<fmt>.cpp; header counts and tokens checked withdetail/parse_guard.hpp; numbers throughdetail/fast_number.hppand streams throughdetail/classic_stream.hpp; parameters throughWriteOptionsandReadOptions. - Registry: one row in
registry_rows()ofsrc/cpp/src/registry.cppand the extension in its defaults table; for an optional dependency, the row inside the feature#ifdefand the library's name inregistry_compiled_out. - Python binding:
core_def_read/core_def_writeinbindings/python/_core.cpp(and a__has_<lib>__flag for an optional dependency). - Python twin:
src/python/meshioplusplus/<fmt>/__init__.py(register_format, shims through_shims.plain_shimsorcore_declined) and_<fmt>.py; export in the package__init__.py(__all__and theTYPE_CHECKINGimports);python tools/gen_format_table.py. - Tests:
tests/cpp/test_<fmt>.cpp,tests/python/test_<fmt>.py(both engines,MESHIOPLUSPLUS_STRICT_CORE=1for the core), aSPECentry intests/python/conformance_spec.pythenpython tools/gen_conformance_table.py, a seed intests/fuzz/seed_main.cpp, and each reader crash fix's input undertests/fuzz/regressions/<fmt>/. - Fixtures:
tests/python/meshes/<fmt>/with a README, the.gitattributesrule, and for third-party files theCITATION.cffparagraph and aCHANGELOG.mdline; never a GPL fixture. - Docs:
doc/formats/<fmt>.md(Reading, Writing, what is not read),doc/formats.md, the sidebar indoc/.vitepress/config.mts, and the format lists ofdoc/c_api.md,doc/wasm.mdand the other binding pages when the registry changes; the MCP server's format capabilities (mcp/_tools.py,tests/python/test_mcp.py,doc/mcp.md). - Generated and release files:
tools/amalgamate.sh; an ABI review row for a new or changed installed header;CHANGELOG.md;README.md; a notebook cell inexample/python/for a user-facing capability; narrow or remove the item here.
Checklist O: an operation.
- Core:
src/cpp/include/meshioplusplus/operations/<op>.hpp(<Op>Optionswithm-prefixed members,<Op>Result {Mesh mMesh; …; std::vector<std::string> mWarnings;},MESHIOPLUSPLUS_API <Op>Result <op>(const Mesh&, const <Op>Options&)) andsrc/cpp/src/operations/<op>.cpp, through the uniform mesh API,parallel_forfor independent loops, shared helpers undersrc/cpp/src/detail/recorded in common utilities. - Python:
m.def("<op>", …)inbindings/python/_core.cpp;src/python/meshioplusplus/_<op>.py(the core call,core_op_declinedbefore a numpy twin, or_core_opfor a core-only operation); three places in the package__init__.py(TYPE_CHECKING,_LAZY_ATTRS,__all__). - C:
mio_<op>_opts(size in its comment, a reserved tail),mio_<op>_opts_init,mio_<op>_report,mio_<op>,mio_<op>_warningsinbindings/c/include/meshioplusplus/meshioplusplus.handbindings/c/c_api.cpp(thecapi_guarded_*helpers and astatic_asserton the struct size). - The flat bindings: Fortran (
bindings/fortran/meshioplusplus.f90: thepubliclist, thebind(c)type, the wrapper); Julia (src/libmio.jlstruct mirror,src/operations.jl, the export list insrc/MeshioPlusPlus.jl); R (R/meshioplusplus.R,src/ops.c,src/init.c,NAMESPACE,man/mio_<op>.Rd); Octave (inst/+mio/<op>.m,src/ops_core.c;tests/python/test_octave_binding_parity.pyholds it to R); WASM (bindings/wasm/js_bindings.cpp,src/wasm/src/index.mjstwice,src/wasm/index.d.ts;tests/python/test_wasm_parity.pyholds it to the C header). - CLIs:
src/python/meshioplusplus/_cli/_<op>.pywith a_Verbrow in_cli/_main.py;cmd_<op>and akCliVerbsrow insrc/cpp/cli/main.cpp; both checked bytests/python/test_cli_json.py. - Pipeline, where the operation fits its contract: the step and its keys in
tools/pipeline_steps.json,python tools/gen_pipeline_table.py, dispatch insrc/cpp/src/operations/pipeline.cppand_pipeline.py(tests/python/test_pipeline_spec.py). - MCP:
tool_<op>inmcp/_tools.py, its@_forwardwrapper inmcp/_server.py,tests/python/test_mcp.py, the count indoc/mcp.md. - Tests:
tests/cpp/test_<op>.cpp,tests/cpp/test_c_api.cpp, aMIO_ABI_LAYOUTline intests/cpp/test_abi_layout.cpp,tests/python/test_<op>.py, and the binding suites (tests/fortran, the Juliaruntests.jl, R'stestthat, Octave'stests/,tests/wasm/smoke.mjs). - Measurement: a
row("<op>", …)insrc/cpp/benchmark/bench_ops.cpp, the determinism check (tools/bench_ops.sh … --hash), andbenchmark/instructions_S.csvre-recorded with the reason. - Docs and release files:
doc/<op>.md, the sidebar, the operations table ofdoc/architecture.md,doc/{cli,c_api,fortran,julia,r,octave,wasm,mcp,pipeline}.md, the ABI row, the single header, a notebook inexample/python/with graphical output,README.md,CHANGELOG.md; narrow or remove the item here.
The map
1. Correctness debts
Admission: behaviour that loses data, mis-orients cells, does not terminate on valid input or fails silently (how this file works). Reopened in November 2026 by the pass that put code anchors on every item, which found the first item below while checking what was then 1.1.2.
1.1 Ansys .rst records flagged both bit-sparse and windowed-sparse are decoded as bit-sparse
Three files of Ansys' DPF example data hold records with both flags set; their layout is neither form's, both engines decode them as bit-sparse without a warning, and pymapdl-reader misreads them too. S to refuse, M to decode.
- State today.
src/cpp/src/formats/ansys_rst.cpp: the flags arekRstBsparse = 0x08andkRstWsparse = 0x10(lines 65–66), andRstFile::Record(lines 103–224) tests bit-sparse first (line 147) and windowed-sparse in theelse if(line 164), so a record with both takes the first branch. The twin does the same:src/python/meshioplusplus/ansys_rst/_ansys_rst.pylines 87–90 (_bsparseline 100,_wsparseline 120). Ansys.rstsays "dense or bit- and windowed-sparse" and does not mention the case. No test covers it (tests/python/test_ansys_rst.pyhasbsparse_doubles,tests/cpp/test_ansys_rst.cppBsparseDoubles). - Steps.
- 1.1.1 Refuse by name. In both engines, a record whose flags hold both bits raises
ReadErrorthroughrst_failnaming the record pointer and the flags, unless 1.1.2 has landed. A hand-built record with both flags is the test in both suites; a fuzz seed goes undertests/fuzz/regressions/ansys_rst/. State it under Not read on the format page. - 1.1.2 Decode it. Find the layout: locate the three DPF example files (
ansys-dpf-core'sexamplesmodule downloads them; record their names and hashes), dump each both-flag record's header and length beside the dense values DPF returns for the same result set, and test the two candidate layouts (a windowed run inside each set bit, or a bit mask over windows). Needs a DPF Server (2.2.3's installation). When one layout reproduces DPF's values on all three, implement it in both engines and replace 1.1.1's refusal.
- 1.1.1 Refuse by name. In both engines, a record whose flags hold both bits raises
- Done when. A file with such a record either raises by name or reads to DPF's values;
pytest tests/python/test_ansys_rst.py -k both_sparsepasses in both engines (MESHIOPLUSPLUS_STRICT_CORE=1for the core).
2. Format reach
Admission: a format a simulation or physics-ML workflow actually exchanges; a shipped format's missing half is not a roadmap item, because every registered format reads and writes (decisions). "Exchanges" means a file that crosses a tool boundary in a real pipeline (mesher → solver, solver → post-processor, solver → training set), not a format that merely exists. Every item names the consumer on the other side of the file.
Sizes: S ≈ days, M ≈ two weeks, L ≈ a month or more, including tests, docs page and CLI wiring. They assume a new format reuses the shared components rather than growing its own: the card tokenizer (detail/keyword_card.hpp), the node-ordering registry, the Fortran record reader (detail/fortran_records.hpp) and the directory sniffing that sequence globs share.
Link legend: unmarked links were opened or returned by a search while this section was written (September 2026); links marked † are quoted from memory and must be checked before relying on them. Vendor documentation portals move often — when a link is dead, search the document title.
Every format this section queued is now read: the native FEM interchange, solver input and result-file readers of v16.5.0 to v16.12.0 (Tecplot, Femap, MFEM, Patran, Abaqus .fil, Radioss, Z88, libMesh, Marc, Ansys .rst, LS-DYNA and OP2), DOLFINx's ADIOS2 .bp (vtx) and Tecplot SZL through TecIO (szplt) of v16.13.0, and the vendor-runtime routes for the files only a vendor's software reads (Abaqus .odb, Marc .t16, Femap .modfem, Ansys results beyond the native reader). Since v16.17.0 the shared components are used wherever a format's layout fits them, the input decks are written too (Radioss's starter deck, checked with the OpenRadioss starter, and Marc's), Femap writes a series as one output set per step, and every format read but not written names its reason on the conformance page, which a test keeps in step with the registry; the missing half of each has since been written, so every registered format reads and writes. What they still lack is a check against a run of the vendor tool, listed under Awaiting a licensed run: take each as soon as a licence, or a file the tool wrote, is in hand.
Considered, not queued
- Graphics, CAD, VFX or lab-specific rather than simulation interchange: X3D, VRML, Alembic, Silo, FBX, COLLADA, AMF, BYU, Rhino
.3dm, Parasolid, OpenCASCADE.brep. DXF, once listed here, was reopened for sketches and outlines measured from a scan and ships as a drawing format (DXF); 3MF, E57, LAS/LAZ, OpenVDB and OBJ materials, listed here until November 2026, were reopened the same way, because the operations that produce or consume them have shipped since (2.4.5 to 2.4.8). - Building and city models, and GIS vector layers: IFC, CityGML and CityJSON, GeoJSON, Shapefile, GeoPackage. Their consumers want semantics (storeys, attributes, layers) a
Meshdoes not carry, and GDAL and IfcOpenShell serve them; WKT stays the one geometry exchange. Revisit with 9.10.2 if georeferenced meshes become a workflow. - Medical image volumes: NIfTI, NRRD and DICOM are voxel images, not meshes; an array from nibabel or SimpleITK reaches
isosurfacethroughgridtoday. The surface formats of the same pipelines are 2.4.4. - Closed CFD databases: STAR-CCM+
.ccm(libccmio is not redistributable) and.sim; FUN3D and FAST grids beyond what UGRID already reads. - Closed FEM databases with no route worth shipping: Altair
.h3d(undisclosed format, SDK not redistributable — Altair's own advice for OpenRadioss is ANIM → VTK, see the OpenRadioss discussion); HyperMesh.hm; Abaqus.sim. - No native format to implement: deal.II (reads and writes UCD, VTK, Gmsh, Abaqus, Exodus — all reachable today); MOOSE (Exodus).
- Nastran OP4 matrices and punch
.pch: matrix and card dumps, not mesh/field exchange; pyNastran serves them. - Non-MSC Nastran HDF5 dialects: incompatible schemas; revisit with a file in hand.
- Partitioning niche: gmsh
$PartitionedEntities(Kratos partitions through MDPA). - CalculiX
.datprint files:frd.read_datstays a Python helper that returns a list of table dictionaries; it is not aMesh, it has no writer, and.datalready belongs to Tecplot and Marc (CalculiX results). - Operators, not formats (listed only because they came up in the same review): mesh duals other than the tetrahedral median dual (now
tet_to_poly). ICP registration, the cylindrical and spherical transforms and the shell completions, listed here with them, shipped asregister_rigid,convert_coordinatesandmeshioplusplus completion.
Revisit any of them when a consumer asks with a file in hand — a real deck or result file, the tool version that wrote it, and the tool that needs the converted output.
2.1 Open verification items before coding
2.1.1 Every link and call marked † or taken from a reference
- Where they are. In this file: 2.4.1 (the Fluent CFF readers), 2.4.6 (LASzip, laz-perf, libE57Format), 2.4.7 (NanoVDB), 9.1 (Manifold), 9.7 (METIS, Scotch). Outside it: twelve DOIs of mesh generation's further reading (
doc/mesh_cad.mdlines 322–363, legend at line 320) and one accessibility claim indoc/tui.mdline 174. Not marked but of the same kind:femap.feFileWriteNeutral(incontrib/femap/export_neutral.pyline 32 and the$!READDATASETline ofcontrib/tecplot/szplt_to_plt.mcrline 8, both written from the vendor references and never run. - Steps. Open each link and remove its † or replace the link; for a licence claim, quote the licence file's SPDX identifier and the version it was read at. The two vendor calls close with 2.2.4 and 2.2.5.
- Done when.
grep -rn "†" docreturns only the legends. S
2.1.2 MFEM NURBS meshes with non-conforming patches
- State today.
MFEM NURBS NC-patch meshheaders are refused:src/cpp/src/formats/mfem.cpplines 2030–2031 (lex.Fail("'…' meshes are not supported"), after the acceptedMFEM NURBS mesh v1.0/v1.1branch at line 2022) andsrc/python/meshioplusplus/mfem/_mfem.pylines 831–832; tested attests/python/test_mfem.pyline 532 andtests/cpp/test_mfem.cppline 396; stated in MFEM. The NURBS parser ismf_parse_nurbs/MfNurbs(mfem.cpplines 1057–1403), its twinmfem/_nurbs.py. - Scope if a user asks. Read the
patch_topologysection with hanging knots, carry it inNurbsInfo(a new optional member: an ABI Tier A change), sample each patch as today (the non-conformity is between patches, so the sampled cells get hanging nodes: mark them asrefinedoes,refine:hanging), and refuseiga_refineon such a model as it already refuses per-direction refinement of a conforming one. - Done when. MFEM's
data/*-nc*.meshNURBS samples read in both engines. No user has asked: do not start without one. M
2.2 Awaiting a licensed run
Checks that need the vendor tool itself, or a file it wrote that no public source has. Each reader works from the vendor's documentation and every public file found (September 2026); what remains is one probe per line. The procedure is the same for all of them, so it is stated once.
Recording a run.
- Run the probe the item names, on the vendor version it names or a later one.
- If the run produces a file worth keeping and its licence allows redistribution: put it under
tests/python/meshes/<format>/with aREADME.mdin the form oftests/python/meshes/szplt/README.md(a<!--pytest-codeblocks:skipfile-->first line, who wrote the file with which tool and version, a table of files, the licence), add the directory's rule to.gitattributes(filter=lfsfor large binaries as fornastran_op2/*.op2, orbinary -filterto keep small ones out of LFS as forfrdandszplt), and add a paragraph to the comment block at the top ofCITATION.cff(source, copyright, licence, "redistributed unmodified") and a line toCHANGELOG.md. A file that cannot be redistributed is described on the page, with its hash, and a reference.npzof what the vendor tool reads from it is frozen by atools/gen_<format>_reference.pyin the pattern oftools/gen_vtx_reference.py. - Turn the stand-in test into a real one beside it (the table names both), skipped when the fixture is absent, never deleted: the stand-in still guards the script's calls.
- Write the vendor version and the date into the page section the table names; where the table says add, create a
## Checked againstsection as Abaqus.odbhas. - Fix what the run finds in both engines, with the minimized input as a fuzz regression if it was a crash, then remove the item here and its clause in the non-goals' facts of the format if the run turned a fact into a feature.
The vendor-runtime routes of v16.13.0 are tested against stand-ins of the vendor APIs only (tests/python/test_contrib_routes.py, run by the optional-readers job of ci.yml). Each needs one recorded run; what to install for it:
- 2.2.1 Abaqus
.odb. Abaqus 2020 or later (itsabaqus python, whose bundled numpy the script needs; 2023 and before run Python 2.7, 2024 and later Python 3). Runcontrib/abaqus_odb/odb_to_vtu.pyon a job with shells, C3D20R and an assembly-level set, and compare its nodalUand centroidalSagainst the same job's.filread natively (route). - 2.2.2 Marc
.t16. Marc/Mentat 2020 or later with PyPost (py_post, run with Mentat's Python). Runcontrib/marc_t16/t16_to_vtu.pyon a job that writes both.t16and.t19, and compare against the.t19read natively; confirm that post file position 0 holds the model (route). - 2.2.3 Ansys results via PyDPF. A DPF Server (Ansys 2021 R1 or later, or the standalone DPF Server 2024 R2 or later) and
pip install ansys-dpf-core pyvista meshioplusplus[h5py]. Runcontrib/ansys_dpf/rst_to_vtkhdf.pyon a structural.rstand compare against the native.rstreader; then on a/FCOMP,RST,1file, which the native reader refuses (route). - 2.2.4 Femap
.modfem. Femap 2301 or later on Windows, andpip install pywin32. Runcontrib/femap/export_neutral.py, checkfeFileWriteNeutral's argument list against that version's API help, and read the neutral file back (route). - 2.2.5 Tecplot
.szplt. Tecplot 360 2023 or later, andpip install pytecplot. Save a data set as.szpltand as.pltfrom Tecplot itself and read both (the TecIO-backed reader against the native one); runcontrib/tecplot/szplt_to_plt.pyand the macrocontrib/tecplot/szplt_to_plt.mcr; and pointMESHIOPLUSPLUS_TECIO_LIBRARYat Tecplot's own sharedlibteciofor the ctypes reader (route). Also open in Tecplot 360 a file meshio++ wrote with single-precision, 16-bit and byte variables and one with partitioned zones; and, with a TecIO newer than 2018.3 (which refuses to write polygon and polyhedral zones in an SZL file), write such a zone, read it back, and check the written face map against Tecplot's reading, which would turn the refusal of those zones into a reader and a writer.
The readers:
- 2.2.6 Tecplot. A
.pltwritten bypreplotfrom the ASCII fixtures (#!TDV112), read identically to its.dat, and the.pltmeshio++ now writes opened in Tecplot or checked bypreplot; binary files of versions 100–105 and 109–111, of which no sample was found (the VisIt suite covers 71, 75, 106–108). - 2.2.7 Femap. Element (404) and group (408) records written by Femap 10 and by 12 or later (the public 2401 files hold none); the slot layout of the 13-node pyramid (topology 19); a file meshio++ writes, a multi-set series included, imported into Femap.
- 2.2.8 Patran. A neutral file exported by ANSA or HyperMesh.
- 2.2.9 Abaqus
.fil. A binary run with shells and C3D20R, checked against its.datprintout (the truss run TenBarArea is checked); a binary file of a single-precision build (4-byte words), of which no layout description or sample was found, which decides whether the reader and the writer take that width; and a file meshio++ writes, opened in Abaqus. - 2.2.10 Radioss. Animation files of the layouts before OpenRadioss (magic numbers
0x5426to0x542B): neither OpenRadioss's tools nor its readers document them. The 16-bit encoding of the nodal normals (the writer leaves them zero:anim_to_vtkignores them and the encoding is undocumented), and a run of OpenRadioss'sanim_to_vtkon a file meshio++ wrote, time-history lists included. - 2.2.11 Marc. A run of Marc itself, including on a deck or a
.t19post file (a remeshing series included) meshio++ writes (SIZING's fields, one type perELEMENTSline, and the shell and plane-strain default types follow Volume C and Mentat's decks); post files before revision 9 (Marc K7 and earlier, Volume D 2000's PLDUMP layout); Volume A's edge and face numbering, to map edge and face sets (kept as(cell, number)field data) onto facets, which also decides whether side regions can be written to decks and post files; the binary.t16, read or written natively, which needs Volume D's PLDUMP description of it or a sample file (today it is read only through Marc's PyPost, by the route). - 2.2.12 Ansys
.rst. A file written with/FCOMP,RST,1or higher (zlib records):detail/zlib_inflateis ready, the record framing needs a sample. A harmonic (complex) result file, which no public sample or open reader covers (pymapdl-reader takes cyclic mode pairs only and only DPF reads complex sets), so complex results are neither read nor written; and a file meshio++ writes with element rotations and nodal coordinate systems, read by DPF (pymapdl-reader applies the three nodal rotations in the opposite order, so it agrees only on single-axis angles). - 2.2.13 LS-DYNA. A family from LS-DYNA with adaptive remeshing (
NADAPT), thed3thdttime-history database, the cubic and bubble solids (21P, 15T, 20T, 40P, 64), airbag particles and rigid roads (read and written from the manual and lasso-python only: the airbag variable names of a real file, and rigid roads with the full rigid-body velocities,NDIM = 9, which the writer leaves out), a reald3part, thick shells, and the whole reader against LS-PrePost, with the families andbinoutfiles (links included) meshio++ writes opened there; whether LS-DYNA writes a ten-node solid's extra nodes once or twice. - 2.2.14 Nastran OP2. Random element tables, complex SORT2 nodal tables, CBUSH and CGAP forces, CBUSH real stresses, and NX's solid types 300–303 (read from pyNastran's layouts; no sample). Files meshio++ writes, opened in Femap, Patran or an NX/Simcenter post-processor: the writer produces the MSC dialect only, and the element tables it writes beyond pyNastran's reach (the SORT1 PSD, CRM and ATO random tables, the complex layouts pyNastran does not read) follow the reader's layouts.
- 2.2.15 CalculiX
frd.cgx -b(CalculiX GraphiX) reading a binary file meshio++ writes;ccx2paraviewparses the ASCII layout only, so no open tool checks the binary variant beyond meshio++'s own reader and the binary filesccxwrote. - 2.2.16 FEBio
.xplt. FEBio Studio orfebio4opening a file meshio++ wrote: the remeshed series (a mesh section per remeshed step), surfaces with per-surface-node variables, and the compressed states. - 2.2.17 Nastran HDF5. MSC's own post-processors (Patran, Simcenter) reading a file meshio++ wrote: its tables follow the column sets of the six MSC Nastran 2020 files, but the
CBUSH,CGAP,CMASSandCONM1element tables and theMAT5,MAT8,MAT9,PBARL,PBEAMLandPCOMPGtables have no sample, so they are not written; a file with them would add them. - 2.2.18 DOLFINx VTX. ParaView's
ADIOS2VTXReader(tools/gen_vtx_reference.pydrives it) and DOLFINx reading a file meshio++ wrote with several cell types or parts (one block per type and part, withmeshio.shared_points): both read one cell type per file, so this is meshio++'s own extension of the layout; and aVTXMeshPolicy.reusefile (ParaView 6.1 aborts on every step after the first of DOLFINx's own).
Where each run lands (sources are under src/cpp/src/formats/ with a twin under src/python/meshioplusplus/<format>/; tests under tests/python/ and tests/cpp/):
| Item | Script or reader | Version recorded in | Stand-in or existing test |
|---|---|---|---|
| 2.2.1 | contrib/abaqus_odb/odb_to_vtu.py (with contrib/common/mio_vtu_series.py) | route Checked against | test_contrib_routes.py::test_abaqus_odb_route, ::test_abaqus_element_table; compare with test_abaqus_fil.py |
| 2.2.2 | contrib/marc_t16/t16_to_vtu.py | route Checked against | ::test_marc_t16_route (the _PyPost stand-in) |
| 2.2.3 | contrib/ansys_dpf/rst_to_vtkhdf.py | route Checked against | ::test_ansys_dpf_route |
| 2.2.4 | contrib/femap/export_neutral.py | route: add the section | ::test_femap_route_calls |
| 2.2.5 | contrib/tecplot/szplt_to_plt.py and .mcr; TecIO paths of tecplot.cpp | route: add the section; szplt | ::test_tecplot_route_calls; test_szplt.py, test_szplt.cpp |
| 2.2.6 | tecplot.cpp | Tecplot Older versions | test_tecplot_plt.py, test_tecplot_plt.cpp; tools/gen_tecplot_plt_fixtures.py |
| 2.2.7 | femap.cpp | Femap Verification | test_femap.py, test_femap.cpp; tools/gen_femap_fixtures.py |
| 2.2.8 | patran.cpp | Patran: add a Verification section | test_patran.py |
| 2.2.9 | abaqus_fil.cpp | Abaqus .fil Validation | test_abaqus_fil.py, test_abaqus_fil_write.py |
| 2.2.10 | radioss_anim.cpp, radioss_th.cpp | animation Validation, time history Verification | test_radioss_anim.py |
| 2.2.11 | marc.cpp | Marc Validation and Not read | test_marc.py, test_marc.cpp; tools/gen_marc_fixtures.py |
| 2.2.12 | ansys_rst.cpp (the zlib refusal is at lines 112–113) | Ansys .rst Not read and Notes | test_ansys_rst.py; tools/gen_ansys_rst_reference.py |
| 2.2.13 | lsdyna_d3plot.cpp, lsdyna_binout.cpp (no d3thdt reader exists) | d3plot What is not read and Verification; binout Verification | test_lsdyna_d3plot.py; tools/gen_d3plot_reference.py |
| 2.2.14 | nastran_op2.cpp | OP2 Verification | test_nastran_op2.py, test_nastran_op2_write.py |
| 2.2.15 | frd.cpp | CalculiX Binary output | test_frd.py (3.2) |
| 2.2.16 | xplt.cpp | FEBio Notes | test_xplt.py |
| 2.2.17 | nastran_h5.cpp | Nastran HDF5 Verification | test_nastran_h5.py |
| 2.2.18 | vtx.cpp; tools/gen_vtx_reference.py | VTX Validation | test_vtx.py, test_vtx.cpp |
2.3 Depth in formats already read
Constructs a registered format's reader refuses, skips or flattens. Each was located in the code (November 2026), not in a file: verify each first against a file the named tool wrote, and add the fixture with the fix. They are neither a missing direction nor an unwritten feature of something read (the parity decision covers both), except where an item says so; they are parts of the format nothing in meshio++ reads yet. Each lands in both engines by checklist F, with its Reading and Writing sections and its conformance declaration; sources are src/cpp/src/formats/<format>.cpp and src/python/meshioplusplus/<format>/.
2.3.1 CGNS
A CFD mesh arrives without its boundary conditions, its zone interfaces or its structured zones. Consumers: Fluent, STAR-CCM+, Pointwise, SU2 and every CFD post-processor. L in all; the sub-items are independent.
- State today. Two core readers: the raw HDF5 path
cgns.cpp(reads every zone, reads and writesNGON_n/NFACE_nsince v9.21.0) and the cgnslib pathcgns_mll.cpp(every zone too, in zone-name order, since 2.3.1's first step closed), tried first whenMESHIOPLUSPLUS_WITH_CGNSLIB=ON(cgns.cpplines 2141–2158), needed only for ADF containers and the CGNS 3.x inline-length NGON layout (refused by name atcgns.cpplines 389–393). The h5py twin iscgns/_cgns.pyand has no NGON (PARTIAL_ENGINE_EXEMPT["cgns"]intests/python/conformance_spec.py). Conformance (SPEC["cgns"]): cell data missing, regionscellandpoint. - Reuse.
h5::helpers ofsrc/cpp/include/meshioplusplus/detail/hdf5_util.hpp(open_group,group_links,read_dataset,read_attr_string); the file-localcgns_find_child_by_label,cgns_read_c1,cgns_write_node_attrs,cgns_write_c1_node,cgns_read_iterative_data(line 1424) andcgns_read_regions(line 885);detail::vtk_structured_cells(src/cpp/src/detail/vtk_xml_pieces.hppline 20) for the lattice expansion;Region(name, RegionKind::Side, dim, tag, entries(n,2)). - Steps.
- 2.3.1.1 Boundary conditions and families.
BC_t,Family_tandFamilyName_tare ignored without a warning (no code names them). Read eachZoneBC_t/BC_t(PointList/PointRange,GridLocationVertex,FaceCenterorCellCenter,ElementListfor pre-3.3 files) into a region named after the BC, or after itsFamilyName_twhen it has one: aPointregion for vertices, aSideregion(cell, local facet)for faces found by matching the listed face elements to volume facets (detail/cell_faces.hpp), aCellregion when the BC names boundary elements kept as their own block. Keep the BC type (BCWall, …) infield_data["cgns:bc_type:<name>"]. Write them back, which also closes "side regions are not written" (the warning atcgns.cppline 856 and_cgns.pylines 452–454). M - 2.3.1.2 Zone interfaces.
ZoneGridConnectivity_t(GridConnectivity1to1_t,GridConnectivity_t): read the 1-to-1 pairs as point regionscgns:interface:<name>on both zones and offerweldthrough the existingmergepath; abutting and overset interfaces are regions only. M - 2.3.1.3 Structured zones. Refused at
cgns.cpplines 1534–1538,cgns_mll.cpplines 563–566 and_cgns.pylines 845–849. ExpandGridCoordinatesof(ni, nj, nk)to hexahedra or quads withvtk_structured_cells(the twin:vti._vti.lattice_hex_conn), mapFlowSolution_tatVertexandCellCenter, map structured BCs'PointRangeto side regions, and record the dimensions infield_data["cgns:structured:<zone>"]so 8.6's annotation can write the zone back structured. Until 8.6 the writer writes it unstructured and the page says so. M - 2.3.1.4 More of
FlowSolution_t. Rind: a solution padded with rind planes is refused by the row-count check (cgns.cpplines 586–590,_cgns.pyline 169): readRind_tand strip the planes.FaceCenter/EdgeCentersolutions are skipped with a warning (cgns.cpplines 1945–1952): they need 8.4. Solutions and regions of a multi-zone file are not read (cgns.cpplines 1956–1990): concatenate per zone in block order, filling zones without the field asmerge'sFillpolicy does. M - 2.3.1.5 Further bases. Bases after the first are ignored with a warning (
cgns.cpplines 1492–1493). Add abaseread option (name or index, theregion=precedent of OpenFOAM) throughReadOptions; reading all into one mesh stays out, since bases may differ in dimension. S - 2.3.1.6 ADF without cgnslib. Out of scope: ADF is a bespoke binary container and cgnslib is the route; state it on the page and correct the comments that still say polyhedra need cgnslib (
CMakeLists.txtlines 374–386,registry.cpplines 1254–1258,cgns/__init__.pylines 8–12). S
- 2.3.1.1 Boundary conditions and families.
- Tests and fixtures.
tests/python/test_cgns.py,tests/cpp/test_cgns.cpp,tests/cpp/test_hdf5_formats.cpp; fixtures intests/python/meshes/cgns/. The CGNS example set (cgns.github.io"Example files", public domain†) has multi-zone structured files with BCs: take two, with the README of step 2 of recording a run. - Done when.
yf17.cgnsandbump.cgns-class examples read with their BC regions in both engines, andmeshioplusplus infolists them.
2.3.2 Exodus II
Global variables, set variables, polyhedral blocks and assemblies. Consumers: MOOSE (postprocessor values are global variables; its polyhedral output is NFACED), Sierra, Cubit, ParaView's IOSS reader. M
- State today. Verified in the code: neither engine reads any of them. The reader is one loop over the netCDF variables (
exodus.cppfrom line 676, inread_exodus, line 626) with no branch forname_glo_var/vals_glo_var,name_nset_var/vals_nset_var*,name_sset_var/vals_sset_var*or the assembly groups, so they are ignored silently.NSIDEDis refused ("Exodus: unknown element type",exodus.cpplines 720–722,_exodus.pylines 274–276; the mapexodus_to_meshioat lines 67–84 has no entry).NFACEDblocks are stored underfacconn{k}/fbconn{k}, which theconnectprefix test never matches: probably dropped silently, which would be a correctness debt (verify with a file first, and move it to §1 if so).ExodusInfoholdsmInfoRecordsonly (formats/exodus.hpplines 67–76). - Reuse. The file-local netCDF helpers
read_var,var_dims,read_names,read_att_text,read_ints,exo_time_values(exodus.cpplines 151–266), which 2.4.3 moves to a shared header;ExodusTimeSeriesWriter(exodus.hppline 122); the polyhedron kernel forNFACED. - Steps.
- 2.3.2.1 Global variables. Read
vals_glo_var(time, num_glo_var)at the selected step intofield_data["exodus:global:<name>"](theexodus:timeprecedent at lines 896–902); writefield_dataentries of that prefix back, in the series writer too. S - 2.3.2.2
NSIDEDandNFACED.NSIDED:connect{k}withebepecnt{k}node counts per element, to a raggedpolygonblock.NFACED:fbconn{k}/fbepecnt{k}(faces as node lists) andfacconn{k}/ebepecnt{k}(elements as face lists), to apolyhedronblock through theFaceaccessors. Write both; core only if the Python twin cannot stay byte-identical, as CGNS's polyhedra are (then aPARTIAL_ENGINE_EXEMPTentry). M - 2.3.2.3 Set variables.
vals_nset_var{v}ns{s}is a value per node of a node set: scatter into a point array<name>that is NaN off the set, or keep asfield_data["exodus:nset:<set>:<name>"]in set order; decide with 8.4, which gives side-set values (vals_sset_var{v}ss{s}) their home. Until 8.4, read both into thefield_dataform and write them back. S–M - 2.3.2.4 Assemblies. Exodus 8
assemblygroups name a tree of blocks and sets: read into the hierarchy convention 8.5 decides; until then keep the tree inExodusInfoso the writer returns it. S
- 2.3.2.1 Global variables. Read
- Tests and fixtures.
tests/python/test_exodus.py,test_exodus_writer_parity.py,tests/cpp/test_netcdf_formats.cpp. Fixtures: generate withnetCDF4intests/python/exodus_fixture.pyfor 2.3.2.1 and 2.3.2.3; a MOOSE polyhedral output or a file from SEACAS' test data (BSD†) for 2.3.2.2. - Done when.
ncdump -hof a file written back lists the same global and set variable names as the source.
2.3.3 EnSight Gold
Transient geometry, structured parts, several parts on write. Consumers: EnSight and ParaView reading what Fluent, STAR-CCM+, CONVERGE and OpenFOAM's foamToEnsight export. M
- State today.
ensight.cpp/ensight/_ensight.py. Transient (wildcard) geometry is refused (ensight_parse_case, lines 564–566;_ensight.pylines 232–233);change_coords_onlyis named nowhere; a structuredblockpart fails with"expected 'coordinates' record"(line 673;_ensight.pyline 447); only the eight kinds ofkKindsare parsed (lines 493–496;_VARIABLE_KINDS), so complex andper measured nodevariables and themeasured:line are ignored silently; only the first time set is honoured (lines 516–528). The writer (write_ensight, lines 1683–1749) writes noTIMEsection and onepart 1, and ignorescell_data["ensight:part"], which its own reader produces for a file of two or more parts (lines 850–861): a round-trip loss, so 2.3.3.1 falls under the parity decision. - Reuse.
ensight_resolve_wildcard(line 575) already zero-pads*runs for variable files;EnsightCaseInfo(line 420) holds the time values and file numbering;ensight_parse_geo(line 636);detail::vtk_structured_cellsforblockparts. - Steps.
- 2.3.3.1 Write the parts that were read. Group cells by
ensight:partand write onepartper value, named fromfield_dataorPart <n>; variables per part. S - 2.3.3.2 Transient geometry. Resolve the
model:wildcard withensight_resolve_wildcardat the selected step; honourchange_coords_only(connectivity from the first file, coordinates from the step's).time_valuesand the sequence layer need no change. S–M - 2.3.3.3
blockparts. Parseblock [uniform|rectilinear] [iblanked]with itsi j kheader and expand;iblankedto auint8cell mask. S–M - 2.3.3.4 A transient writer, as 2.3.4 describes (the
TIMEsection and numbered files). - 2.3.3.5 Measured and complex variables. Measured data is a point cloud with its own files: read as a second vertex block when a
measured:line is present. Complex variables wait on 8.1's naming. S
- 2.3.3.1 Write the parts that were read. Group cells by
- Tests and fixtures.
tests/python/test_ensight.py,tests/cpp/test_ensight.cpp; fixtures intests/python/meshes/ensight/. Generate transient andblockcases from ParaView'sEnSightWriteror VTK's test data (BSD), and fix the stale comment atensight.cpplines 404–409 (tensors are read). - Done when. A two-part transient case from ParaView reads at every step and writes back with both parts.
2.3.4 A series writer for every format that reads one
MED, CGNS, Tecplot, Gmsh, EnSight and UNV read time steps, but a sequence written to them is refused, although each has an in-file timeline. S each
- State today.
sequence_write_supports_time(src/cpp/src/operations/sequence.cpplines 282–306) lists 20 formats; these six are absent, thoughseq_format_may_have_steps(line 219) lists them on the read side.TEST(SequenceCapability, WriteSupportsTimeAgreesWithReality)(tests/cpp/test_sequence.cppline 401) holds the list to what the sinks do. - The four places to touch per format. In
sequence.cpp: the||chain and its message;kSeqSeriesFormats(lines 592–611);seq_make_series_sink(lines 696–770,SeqStepSink<TWriter>for a per-stepWrite(time, mesh)writer,SeqSink<TWriter, true>for a static-grid one). In Python:_SERIES_WRITERSand thewrite_sequencebranch (src/python/meshioplusplus/_sequence.pylines 108–130 and from 850), with a shim onCoreSeriesWriter(_series_writer.py). The writer class copiesFrdSeriesWriter(formats/frd.hpplines 157–183: constructor,MESHIOPLUSPLUS_SERIES_WRITER_SPECIAL_MEMBERS,Write,NumSteps,Finalize, a pimpl), which is a pure ABI addition (a new class in an existing header: an ABI review row). C, Fortran, Julia, R, Octave and WASM reach it through the sequence entry points with no new symbol. - Per format.
- 2.3.4.1 MED. The Python writer already writes several
CHAsteps, from array names"Name[idx] - pdt"orfield_data["med:step_meta"](med/_med.pylines 930–990, routed bymed/__init__.pylines 111–150), but is not in_SERIES_WRITERS; the core writes one step withNDT = -1(med.cpplines 1820–1823, its comment deferring to Python). Give the core the same step groups, then wire both. - 2.3.4.2 CGNS. Write
BaseIterativeData_t(TimeValues) andZoneIterativeData_t(FlowSolutionPointers) with oneFlowSolution_tper step; the read side iscgns_read_iterative_data(cgns.cppline 1424). - 2.3.4.3 Tecplot.
SzWriter::AddStep(mesh, Unsteady, Time)(tecplot.cpplines 3084–3130) already writes one zone per step with a strand, for TecIO only;write_tecplot(line 2604) writes noSOLUTIONTIMEorSTRANDID. Write them in the ASCII and.pltzone headers, sharing the grid withVARSHARELIST/CONNECTIVITYSHAREZONEwhen it does not move. - 2.3.4.4 Gmsh. The time tag of
$NodeData/$ElementDatais hard-coded 0 (gmsh.cpplines 2140 and 2176): write one data section per step with its time and step index. - 2.3.4.5 EnSight. The
TIMEsection (number of steps,filename start number,time values) and one numbered variable file per step (ensight_resolve_wildcardgives the names); geometry once, or per step for a moving mesh. - 2.3.4.6 UNV. The writer already labels each 2414 dataset from
unv:analysis,unv:stepandmeshio:time(unv.cpplines 1995–2046): repeat that block per step.
- 2.3.4.1 MED. The Python writer already writes several
- Done when. For each: a three-step
write_sequencereads back with its three times (a parametrized case intests/python/test_sequence.pyandtest_sequence.cpp), and the capability test passes.
2.3.5 OpenFOAM
Sets, particle clouds, and compressed writes beyond Python and C++. S–M
- State today.
openfoam.cpp(4028 lines) /openfoam/_openfoam.py(single-regionpolyMeshonly,PARTIAL_ENGINE_EXEMPT["openfoam"]). Both readers takewriteCompression oncases (points.gz, …) andwrite_openfoamwrites them throughOpenFoamWriteOptions::mCompressand Python'scompression="gzip"(compressed cases); nothing namessetsorlagrangian. Tests build cases inline (tests/python/test_openfoam.py,tests/cpp/test_openfoam.cpp); there is no fixture directory. - Steps.
- 2.3.5.1
polyMesh/sets.cellSet,faceSetandpointSetarelabelLists like zones: parse withparse_zone_file_binary(line 624) and the ASCII zone path into regionsset:<name>(face sets to side regions through the face owner, as face zones). Write regions of that prefix back as sets. S - 2.3.5.2
lagrangianclouds.<time>/lagrangian/<cloud>/positions(barycentricpositionssince v1806, or Cartesiancoordinates) and the per-particle fields: a vertex block with point data, returned as a second mesh through acloud=read option, since a particle is not a node of the mesh. Read only; apositionsfile needs the owning cell, which a writer would have to locate (9.3). M - 2.3.5.3 Compressed writes on the flat surfaces. The gzip write reaches only Python and C++, like the label and scalar widths: the registry's
registry_write_ex(write_options.cpp, theopenfoambranch) buildsOpenFoamWriteOptionsfromWriteOptions, which has no compression field. Add one (an ABI change toWriteOptions, andmio_write_optsfrom its reserved tail), then--compression gzipin both CLIs and the WASM and flat-binding write options. S
- 2.3.5.1
- Done when. A tutorial case's
setsand alagrangiancloud read in the core, and ablockMesh+simpleFoamcase written by OpenFOAM withwriteCompression onreads equal to its uncompressed twin (the synthetic gzipped cases ofOpenFoamCompressedintests/cpp/test_openfoam.cppstand in until one is committed).
2.3.6 Gmsh
- State today.
$ElementNodeData,$InterpolationSchemeand$GhostElementsfall into the generic unknown-section skip with no warning:gmsh.cpplines 1431–1432 (4.x body) and 1912–1913 (2.2),gmsh/_gmsh22.pylines 74–75,_gmsh41.pylines 91–100,_gmsh40.pyline 83. - Steps.
- 2.3.6.1
$ElementNodeData. A value per element per node (discontinuous fields: what Gmsh's own post-processing and GetDP write). Parse with the tag readerread_data(gmsh.cppnear line 943;common._read_datain Python) and store as cell data of shape(cells, nodes × components)under the convention 8.3 decides; until then undergmsh:element_node:<name>with the layout infield_data, the formabaqus:layout:uses. Write it back. S–M, after 8.3's design pass - 2.3.6.2
$GhostElements. Tocell_data["partition:ghost"], the namepartition's ghost layers use. S - 2.3.6.3
$InterpolationScheme. Kept only as an opaqueGmshInfomember so a round trip does not lose it. S
- 2.3.6.1
- Done when. Gmsh's
tutorials/view*.msh-class file with element-node data round-trips in both engines.
2.3.7 The structured VTK family
- State today.
read_vtkhdfrefuses everyTypebutUnstructuredGrid,PolyDataand the two composites (vtkhdf.cpplines 1636–1638;vtkhdf_is_leaflines 163–165;vtkhdf/_vtkhdf.pyline 830)..pvts,.pvtrand.pvtihave no reader.vti,vtrandvtsfail conformance with"no cell type round-trips"(conformance_spec.py,SPEC["vti"],["vtr"],["vts"]), because the canonical mesh is not a lattice;vtrneeds a uniform one (vtr.cpplines 50–52). - Reuse.
detail::vtk_xml_read_piecesandvtk_structured_cells(src/cpp/src/detail/vtk_xml_pieces.hpp);detail::lattice_from_meshandLatticeSpec(detail/grid_lattice.hpplines 101–205); the parallel-index pattern ofpvtu.cpp(pvtu_infoline 70,pvtu_parse_indexline 484) withpidx::merge_pieces(src/cpp/src/formats/pindex_common.hppline 272), and in Python_parallel_xml.make_parallel_formatand_pvtk_index.py. - Steps.
- 2.3.7.1 VTKHDF
ImageData.WholeExtent,Origin,Spacing,Directionattributes andPointData/CellDatagroups: expand asvtidoes. ThenRectilinearGridandStructuredGridwhere the VTKHDF specification defines them at the pinned VTK version (verify first: they were proposals in 2025). S–M - 2.3.7.2
OverlappingAMR. Levels of boxes with a refinement ratio: read as one hexahedral block per level withvtkhdf:amr:levelcell data and the coarse cells under finer boxes blanked into auint8mask; no hanging-node closure.HyperTreeGridstays refused: it is a tree encoding with no cell list. M - 2.3.7.3
.pvts,.pvtr,.pvti. Add three rows topvtu_info, reading pieces with the serial readers and placing each by itsExtentin theWholeExtentso shared planes are one layer of points (the existing merge does not weld). Writers split bypartition:partonly if the parts are boxes; otherwise refuse by name. M - 2.3.7.4 The round trip is 8.6's: with its annotation, a mesh read from
vtswrites back asvts, and the three conformance declarations change fromerrorto a note.
- 2.3.7.1 VTKHDF
- Done when. VTK's
Testing/Datastructured.vtkhdfand.pvtisamples read to the same point and cell counts asvtkXMLReaderreports.
2.3.8 Decomposed files, read whole without MPI
Reopened from the MPI non-goal, which stays as it is for the library: gathering the pieces of a finished run is file reading. M
- State today. Four formats reassemble, each with its own code: OpenFOAM (
reconstruct_decomposed,openfoam.cppline 863, by the*ProcAddressinglists),pvtu(pidx::merge_pieces: no global ids, ghosts dropped, pieces labelled aspiece_iregions rather thanpartition:part), Elmer (elm_read_parts,elmer.cppline 541: nodes deduplicated by global id through a hash map,partition:partset) and MFEM (mf_read_parallel,mfem.cppline 3372). Nothing names Nemesis,node_num_maporelem_num_map; the Exodus readers ignore the number maps.MergeOptions(operations/merge.hpplines 66–70) welds by tolerance only. - Steps.
- 2.3.8.1 One helper.
detail/merge_by_global_id.{hpp,cpp}: pieces with a global node id per local node and optionally a global cell id, to one mesh with shared nodes merged, cells in global order when ids are given andcell_data["partition:part"](kPvtuPartKey,formats/pvtu.hppline 80). Move Elmer's and MFEM's loops onto it with digests unchanged (tools/bench_ops.sh --hash), record it in common utilities, and givepvtuthepartition:partarray beside its regions. S–M - 2.3.8.2 Nemesis. A base name with
.e.<N>.<p>siblings (zero-paddedp): sniff the family as the directory sniffing of sequence globs does, read each withread_exodus, takenode_num_mapandelem_num_mapas the global ids, and merge node sets, side sets and variables. Areadof any one member reads the family unlesspiece=selects one. Writing a family frompartition:partfollows, as OpenFOAM'sdecomposed=True. M - 2.3.8.3 CGNS and MED one file per rank. Only with a file in hand: neither has one convention for it.
- 2.3.8.1 One helper.
- Done when. A four-rank set from SEACAS
decompreads equal, after sorting by global id, to the serial file it was cut from.
2.3.9 Smaller losses, one fix each
Each is the parity decision's "unwritten feature" if the format can hold it; where it cannot, the page says so and the item closes. S each
- 2.3.9.1 glTF morph targets.
write_gltfwrites one static mesh, and the reader skips morph targets (gltf_read.cpplines 799–800) and animations (lines 832–836). Write a series of point displacements astargets(POSITIONdeltas) with ananimationsampler on theweights, so a deformation series is one.glba browser plays; read them back as steps. A series sink as in 2.3.4. M - 2.3.9.2 STL solid names and attribute bytes.
solid <name>lines are treated as comments (stl.cpplines 116–120,stl/_stl.pyline 120); the two attribute bytes of a binary facet are skipped on read (lines 193–202) and written 0 (lines 365–366). Several ASCII solids become cell regions named after them and are written back as several solids; the attribute word becomescell_data["stl:attribute"](uint16) when any is non-zero. - 2.3.9.3 OBJ normal indices.
vnrows are kept aspoint_data["obj:vn"]and written back; the per-cornervnindex is parsed past and dropped (obj.cpplines 184–196,_obj.pyline 124), so normals that are not one per vertex are scrambled. Split points on distinct(v, vn)pairs asvtdoes (obj:parent_point). - 2.3.9.4 PLY. List-typed vertex properties are refused by the core (
ply.cpplines 211–212); the twin's header regex andply_to_numpy_dtype["list"](ply/_ply.pylines 111 and 182) look unable to read them either, against what the page says: test first.int64properties are kept; the loss is connectivity cast toint32on write (ply.cpplines 489–494,_ply.pylines 473–483): refuse above 2³¹ rather than wrap, or writeint64lists behind an option. - 2.3.9.5 Medit normals.
Normals,NormalAtVertices,TangentsandTangentAtVerticesare discarded (kMeditCountedSkips,medit.cpplines 570–579;medit/_medit.pylines 438–448 and 497–505).NormalAtVerticesmaps vertices to rows ofNormals: read topoint_data["medit:normal"]and write back. The engines also disagree on the skip itself, which is 3.4.12. - 2.3.9.6 Regions and data dropped on write.
mphtxtdrops point and side regions (mphtxt.cpplines 702–705) and all data (line 874); MED side regions (med.cpplines 901–904);frdregions and non-nodal arrays (frd.cpplines 1093–1097 and 1287). For each, decide from the format's documentation whether it has a slot (COMSOL boundary selections; MED families on face elements; CalculiX has none) and write it or state the fact on the page. - 2.3.9.7 LS-DYNA
*ELEMENT_SOLID_H20. Skipped as unsupported (lsdyna.cpplines 419–422,lsdyna/_lsdyna.pylines 266–267); the two-line layout reads at most ten nodes. Read 20 nodes tohexahedron20through the node-ordering registry.
2.4 New formats
Each names its consumer, as the admission asks. None was estimated from a file: verify first with one in hand, and with the licence of whatever reader the layout is taken from. Each lands by checklist F.
2.4.1 Fluent case and data
- Why. The
.mshmesh is read (ansys), but not a.cas/.datpair nor the HDF5.cas.h5/.dat.h5(CFF) Fluent 2020 and later write by default: a Fluent user cannot bring a solution to meshio++ without exporting it first. Consumer: Fluent → ParaView, OpenFOAM or a training set. - Reuse.
ansys.cpp:struct FluentReader(line 133: bracket depth, hex header groups, binary bodies),read_ansys(line 347: section index split into the binary-width prefix20/30and the core id; cores 10, 12 and 13 are nodes, cells and faces; 39 and 45 zone names),fluent_hex(284),fluent_face_rows(324); the twin's_Reader(ansys/_ansys.pyline 53). HDF5 throughdetail/hdf5_util.hpp. - Steps.
- 2.4.1.1 Legacy
.cas. The same grammar as.mshwith more sections: register.casonansys' reader, skip the solver sections by index, keep zone types (section 45) as region names andfield_data["fluent:zone_type:<name>"]. S - 2.4.1.2 Legacy
.dat. Sections 300, 2300 and 3300 ((300 (subsection zone size ntime nphase first last) (values…))): cell-zone values to cell data, face-zone values to the boundary blocks; the sub-section id to variable name table is in Fluent'sxfile.h(documented in the Fluent User's Guide appendix; names only, no code). A.casread looks for the sibling.dat, anddata=names one. M - 2.4.1.3 CFF
.cas.h5/.dat.h5. Undocumented; the open descriptions are VTK'svtkFLUENTCFFReader(BSD-3†) and PyFluent's file readers (MIT†): read the layout from them, not their code paths verbatim, and credit both inTHIRD_PARTY_NOTICES.mdonly if code is taken./meshes/1/{nodes,faces,cells}and/results/1/phase-1/{cells,faces}/<field>/1. A new format name (fluent_h5), gated on HDF5 ascgnsis. M - 2.4.1.4 Writing. The parity decision asks for it: a case the reader returns is written back as legacy
.cas/.dat(the.mshwriter plus the data sections); CFF write only if Fluent is at hand to open it, otherwise aREAD_ONLYconformance entry with that reason and an item in 2.2.
- 2.4.1.1 Legacy
- Fixtures. Fluent tutorial cases are not redistributable; VTK's
Testing/Datahas small.cas/.datand.cas.h5files (BSD): take those with their README. - Done when. VTK's
room.cas/room.datandroom.cas.h5/room.dat.h5read to the cell counts and field rangesvtkFLUENTReaderandvtkFLUENTCFFReaderreport. M (legacy), M (CFF)
2.4.2 PLOT3D
- Why. Multi-block structured grids (
.xyz/.x/.g, solution.q, function.f): what OVERFLOW, CFL3D and NASA's turbulence-modelling resource exchange. - Reuse.
detail/fortran_records.hpp:sniff_fortran_records,fortran_records,fortran_append_record,fortran_put_*(twin_fortran_records.py);vtk_structured_cells; thebyteswap.hpphelpers. - Steps. (1) Sniff the variant, since the file does not state it: ASCII or Fortran-unformatted or raw binary, single or multi-block, 2-D or 3-D, single or double precision, with or without
iblank, either byte order;sniff_fortran_recordsgives the record layout and the first record's size the rest. An ambiguous file takes read options (precision=,multiblock=,iblank=), named in the error. (2) One block per grid block, expanded to hexahedra or quads,iblankto auint8point array, block dimensions infield_data["plot3d:dims"]for 8.6. (3).q: the four reference values tofield_data, the five conserved variables to point data..f:nnamed-by-index functions. (4) Writer: the unformatted multi-block double-precision form, from blocks that carryplot3d:dims; a mesh that is not a set of lattices is refused by name. - Fixtures. NASA TMR grids (US-government works).
- Done when. A TMR multi-block
.p3dfmtgrid reads to its documented block dimensions and writes back byte-equal in the unformatted form. S–M
2.4.3 Coastal and ocean meshes
- Why. Triangles and mixed polygons with nodal fields and a timeline, all of which the model holds. Consumer: a hydrodynamic run → ParaView, QGIS's mesh layer or a surrogate's training set.
- Steps.
- 2.4.3.1 A shared netCDF helper first. netCDF is used by
exodus.cppalone and its helpers are file-local (check,read_var,var_dims,read_names,read_att_text,read_ints; lines 58–266). Move them tosrc/cpp/src/detail/netcdf_util.{hpp,cpp}(core-private, in the manner ofdetail/hdf5_util.hpp), with Exodus on them and its digests unchanged; record it in common utilities. S - 2.4.3.2 UGRID-conventions NetCDF. The UGRID conventions: a variable with
cf_role = "mesh_topology"namesnode_coordinatesandface_node_connectivity(withstart_indexand a fill value for mixed polygons), optional edge and volume connectivities, and data variables withmeshandlocation(node,face,edge) attributes over atimedimension. Format nameugrid_nc(the AFLR3ugridkeeps its name);.ncis sniffed by the attribute, since Exodus and others share the extension. Delft3D-FM, SCHISM, FVCOM, D-Flow and xugrid write it. Sphericallon/latcoordinates are kept as they are with the CRS name infield_data(9.10.2). Edge data needs 8.4. Python twin throughnetCDF4. M - 2.4.3.3 ADCIRC.
fort.14(ASCII: title, counts, nodes with depth, triangles, open and land boundary node strings to point regions) withfort.63/fort.64ASCII results as steps; the NetCDF forms through 2.4.3.2's reader where they follow CF. Format nameadcirc. S - 2.4.3.4 TELEMAC Selafin.
.slf: Fortran big-endian records (fortran_records): title, variable names and units,IPARAM, counts,IKLEconnectivity,IPOBO, coordinates, then per step a time and one record per variable; single or double precision by record size. Format nameselafin. Units feed 9.10.1. S–M
- 2.4.3.1 A shared netCDF helper first. netCDF is used by
- Fixtures. The UGRID conventions' own examples; ADCIRC's public example problems; TELEMAC's examples (GPL data: do not copy, generate small files with the writer once it exists and check against a TELEMAC-written one locally).
- Done when. A Delft3D-FM
_map.nc, an ADCIRC quarter-annular case and a TELEMACr2d_*.slfeach read with their times in both engines. M for the three
2.4.4 Neuroimaging surfaces
- Why. Head models for TMS and EEG simulation (SimNIBS, Brainstorm, MNE) mesh these surfaces into volumes;
tetrahedralizeis the next step of that pipeline. Lowest of this group: take it when a user asks. - Steps. (1) GIFTI (
.gii): XML through pugixml (src/cpp/third_party/pugixml),DataArrayelements withIntent(NIFTI_INTENT_POINTSET,_TRIANGLE,_SHAPE,_LABEL, …),EncodingASCII,Base64BinaryorGZipBase64Binary: decode withb64decode(detail/vtu_binary.hpp) thenzlib_inflatewith window 15 (detail/zlib_inflate.hpp), not the VTK block framing, which is a different header;ArrayIndexingOrderandEndianhonoured; the coordinate-system matrix tofield_data; label tables to regions. (2) FreeSurfer: binary triangle surfaces (magic0xFFFFFE, big-endian),curvfiles (magic0xFFFFFF) as point data,annotas regions; one formatfreesurferwith the sibling files as read options. (3) Writers for both. - Fixtures. The GIFTI reference files of the NITRC GIFTI project (public domain†); a FreeSurfer
fsaveragehemisphere is under the FreeSurfer licence (do not copy: generate with nibabel in the test,importorskip). - Done when.
nibabelreads what meshio++ wrote and the reverse. S–M
2.4.5 3MF
Reopened from Considered, not queued: print preparation is now a meshio++ workflow (repair, offset_surface's thicken and hollow, remesh, the scan-to-part loop), and 3MF is what slicers exchange, with units, several objects and colour where STL has none. Consumer: PrusaSlicer, Cura, Bambu Studio. M
- Reuse. A ZIP reader exists but refuses more than one member:
cmpin_zip_entryandcmpin_unzipinsrc/cpp/src/detail/compressed_input.cpp(lines 199–264; the refusal at line 237; no Zip64). For writing there is a CRC-32 (png_crc32,src/cpp/src/detail/png_write.hppline 39) and deflate through zlib (png_write.cppline 139). pugixml for the XML. In Python,zipfile. - Steps.
- 2.4.5.1 A ZIP container in the core.
detail/zip_archive.{hpp,cpp}: list members and read one by name (lifting the single-member limit of the existing parser, which then calls this), and write stored or deflated members with a central directory. No Zip64 (refuse above 4 GB by name), no encryption. Tests againstzipfile-written archives; fuzz it through the 3MF reader's seed. Recorded in common utilities. S–M - 2.4.5.2 The core specification. Read
_rels/.relsto find the model part (3D/3dmodel.model), then<resources>: each<object type="model">with a<mesh>(<vertices>,<triangles v1 v2 v3 pid p1…>) is a triangle block and a cell region named after the object;<components>and the<build>items'transformare applied (transform),unitgoes tomeshio:length_unit.<basematerials>and per-trianglepid/p1to auint8RGBA cell array3mf:color. The writer emits one object per cell region,[Content_Types].xmland the relationships. M - 2.4.5.3 Out of scope. The materials, production, slice and beam-lattice extensions; a model using a
requiredextensionsthe reader lacks is refused by name.
- 2.4.5.1 A ZIP container in the core.
- Fixtures. The 3MF Consortium's sample files (BSD-2†).
- Done when. The consortium's validator (
lib3mf's, run outside the tests) and PrusaSlicer open a file meshio++ wrote; the consortium's core samples read.
2.4.6 E57 and LAS/LAZ
Reopened: scan registration, surface reconstruction, deviation and the fitting operations shipped, and their input today is pcd, ply or xyz, while a terrestrial scanner writes E57 and an airborne one LAS. M
- Reuse.
pcd.cpp's shape for a point cloud:pcd_build_mesh(line 605) builds onevertexblock (line 686), unpacks colour and keeps sensor metadata infield_data; conformance note "every node is kept as a vertex" (SPEC["pcd"]). - Steps.
- 2.4.6.1 LAS, native. A fixed binary layout: the public header (versions 1.2 to 1.4, with scale and offset), variable-length records (the CRS as GeoKeys or WKT, kept verbatim for 9.10.2) and point records of formats 0 to 10:
X Y Zscaled to doubles,intensity,classification,return_number,gps_time, colour. Read options to subsample (every=) and to pick fields. Writer: format 6 or 7 of LAS 1.4. Format namelas. S - 2.4.6.2 LAZ. Compressed LAS through LASzip or laz-perf (Apache-2.0†), an optional dependency in the TecIO pattern: a CMake option
MESHIOPLUSPLUS_WITH_LAZ, the feature macro, the registry row inside the#ifdef,registry_compiled_outreturning the library's name so the refusal names it (registry.cpplines 1236–1259),__has_laz__in_core.cpp, the twin throughlaspy[lazrs], and a leg in theoptional-readersjob. S–M - 2.4.6.3 E57. XML header plus compressed binary sections; through libE57Format (BSL-1.0†) with the same gating (
MESHIOPLUSPLUS_WITH_E57), the twin throughpye57. One vertex block perdata3Dscan with its pose applied, or kept apart withscan=; intensity and colour to point data; images are not read. M
- 2.4.6.1 LAS, native. A fixed binary layout: the public header (versions 1.2 to 1.4, with scale and offset), variable-length records (the CRS as GeoKeys or WKT, kept verbatim for 9.10.2) and point records of formats 0 to 10:
- Fixtures. ASPRS sample LAS files and libE57Format's test data, each with its licence checked.
- Done when.
meshioplusplus info scan.lasandscan.e57list the points and fields, andregister_rigidruns on two E57 scans.
2.4.7 OpenVDB, through NanoVDB
Reopened: compute_sdf, voxelize and octree produce the sparse level sets and fog volumes it stores, and physics-ML pipelines and Blender consume them. M, design pass first
- State today.
compute_sdfreturns a hexahedral lattice mesh withsdf:distancepoint data and the lattice infield_data(sdf:origin,sdf:spacing,sdf:dims;operations/sdf.hpplines 252–256), dense or as a 1-irregular octree (SdfStructure);LatticeSpecandlattice_from_meshare indetail/grid_lattice.hpp. - Decisions to take first. (1) Whether NanoVDB's header (verify its licence, Apache-2.0 or MPL-2.0†, and that it is self-contained) reads and writes
.nvdbonly, which is what its viewers and Blender's importer need to be checked for, or whether.vdbproper is required, which means the OpenVDB library and its dependencies (then a provider add-on, not the core). (2) The shape on the meshio++ side: recommended, an export of a lattice array (a narrow band ofsdf:distance, orvoxelize's occupancy) and an import to a lattice mesh, registered as a formatnvdbwhose writer refuses a mesh that is not a lattice by name, asvtidoes. - Done when. Blender or
nanovdb_vieweropens a narrow-band level set written fromcompute_sdf's output.
2.4.8 Materials and textures
Reopened: parameterize now produces texture coordinates, so a baked field or a scan's colour can travel as an image. S–M
- State today. Texture coordinates already round-trip:
uv(kUvName,operations/parameterize.hppline 70) to OBJvtwithf v/vtcorners (obj.cpplines 225–303) and to glTFTEXCOORD_0withvflipped (gltf.cpplines 939–971). Missing: OBJ writes nomtllib/usemtland reads none; glTF writes one fixedbaseColorFactormaterial and no image (lines 1308–1340) and skips textures on read (gltf_read.cpplines 835–836). A PNG encoder exists (png_encode_rgba,detail/png_write.hppline 49); there is no PNG decoder. - Steps. (1) glTF: a
texture=write option taking an RGBA array or a PNG path, embedded as a buffer view in.glband referenced asbaseColorTexture; on read, keep the image bytes in a side channel (GltfInfo) and its texture index, undecoded, so a round trip returns it. (2) OBJ: writemtlliband oneusemtlwith a sibling.mtl(map_Kd) and the PNG beside it; readusemtlgroups as cell regions and the.mtl'sKdandmap_Kdintofield_data. (3) Abake_texture(mesh, array, size)helper that rasterizes a point or cell array throughuvwith the existing rasterizer and colormaps (detail/raster), so a field becomes an image: Python and the core, by checklist O. No material model beyond base colour. - Done when.
gltf-validator(thegltf-validatorCI job) accepts a textured.glb, and a parameterized mesh with a baked field opens textured in Blender.
3. Quality of implementation
Admission: work that makes every other item safer to land. None of it is a feature, so none of it competes for the same attention — it can run in parallel with everything.
The sanitizer leg, the reader fuzzing, the format conformance matrix, the property tests, the benchmark harness, the ParaView plugin test and the fallback narrowing all shipped in v16.14.0 (fuzzing and sanitizers, format conformance, benchmarks). What remains, each item with the files it touches; the build is one CMakeLists.txt (2,289 lines) and CI one ci.yml, so most items name lines in those two.
3.1 OSS-Fuzz submission
Calendar-bound, like the registries in §4. S
- State today. The maintainer's Google-account contact is confirmed, the submission files pass OSS-Fuzz's presubmit checks, and the helper-container image and external-engine ASan build pass. The files are
tools/fuzz/oss-fuzz/{Dockerfile,build.sh,project.yaml,package_seeds.py}: the image clonesmasterwith the Eigen and JSON submodules;build.shconfigures with-DMESHIOPLUSPLUS_FUZZING_ENGINE="$LIB_FUZZING_ENGINE", HDF5, netCDF, gidpost and bzip2 off and the SEQ backend, copiesmeshioplusplus_fuzz_readonce per format (frommeshioplusplus_fuzz_replay -list-formatsminustools/fuzz/not_fuzzed.txt) and packs the seeds;project.yamlasks for address and undefined with libFuzzer. Fuzzing holds the helper commands, not a checklist;tests/python/test_fuzz_tools.pytests the tools; no workflow touches the directory. - Steps.
- 3.1.1 In a checkout of
google/oss-fuzzwith the four files underprojects/meshioplusplus/:python3 infra/helper.py build_image meshioplusplus, then foraddressandundefined:build_fuzzers --clean --sanitizer <s>,check_build --sanitizer <s>,run_fuzzer --sanitizer <s> meshioplusplus meshioplusplus_fuzz_read_vtu -- -max_total_time=30. - 3.1.2
build_fuzzers --clean --sanitizer coverage, unpack one seed corpus,coverage --no-corpus-download --no-serve --fuzz-target=meshioplusplus_fuzz_read_vtu, and read the report for readers with near-zero coverage (a missing seed or dictionary: add it totools/fuzz/seed_corpus.pyortools/fuzz/dicts/). - 3.1.3 Add the two operation targets to
build.sh(meshioplusplus_fuzz_mesh_cad_surface,meshioplusplus_fuzz_triangulate_polygon_2d; it builds the reader copies only) with their seeds fromtests/fuzz/regressions/op_*. - 3.1.4
SECURITY.mdfirst (3.14.1): OSS-Fuzz files reports against it. Then open the pull request ingoogle/oss-fuzz, answer review, and turn the command block of the page into the record of what ran and when.
- 3.1.1 In a checkout of
- Done when. The project builds on oss-fuzz.com and its first reports arrive.
3.2 CalculiX validators in CI
- State today.
tests/python/test_frd.py::test_ccx2paraview_reads_what_the_writer_wrote(line 1404, five parametrized meshes) andTestBinaryWriter::test_ccx2paraview_cannot_read_the_binary_layout(line 1217) skip onimportorskip("ccx2paraview")andimportorskip("vtk"); no workflow installs either. CalculiX names ccx2paraview 3.2.0. - Steps. Add a
frd-validatorjob toci.ymlin the pattern ofgltf-validator(lines 1935–1970:needs: [lint], Python 3.12,pip install .plus the validator, one test file):pip install . ccx2paraview vtk pytest,pytest tests/python/test_frd.py, withMESHIOPLUSPLUS_REQUIRE_FRD_VALIDATOR=1turning the skip into a failure as_require_validatordoes for glTF (tests/python/test_gltf.pylines 733–738). Thefrdfixtures are outside LFS, so the checkout needs none. Pin the ccx2paraview version and name it on the page. - Done when. The job is green and fails when ccx2paraview is uninstalled from it. S
3.3 The meshing corpora: sources and first numbers
The CAD corpus (benchmarks, tools/cad_corpus.py) and the Thingi10K sample tetrahedralize is measured on (tools/tet_corpus.py) are weekly jobs; three pieces need a run that can reach the sources, which this repository's development sandboxes could not. S
- State today.
cad_corpus.pyhasfetch(--manifest --dest --record),run,compare,baseline,table --checkandsummary; its manifesttools/cad_corpus/manifest.jsontakesgitandhttpssources;fetchwrites the hash of a model that has none to--recordand refuses a mismatch;fetch_https(lines 176–189) extractsziponly.cad_corpus.ymlruns Mondays and uploadscorpus-hashes.jsonin thecad-corpusartifact.tet_corpus.pyhasfetch,run,summary;tools/tet_corpus/manifest.jsonhas"models": {}, a seeded sample of 300 and the Hugging Face Thingi10K v1.5.0 tarball;tet_corpus.ymlruns Tuesdays and uploadstet-corpus-hashes.json. Tests:tests/python/test_cad_corpus.py,test_tet_corpus.py. - Steps.
- 3.3.1 NIST's MBE PMI test models (a US-government work): add an
httpssource for the CTC and FTC STEP archives with no hashes, let the scheduled run record them, and commit the recordedsha256values from the artifact. - 3.3.2 ABC (Koch et al. 2019; MIT dataset, models under Onshape's public-document terms, which permit fetch-at-run-time): its chunks are 7-Zip archives. Add
archive: "7z"tofetch_httpsthroughpy7zr(LGPL, a CI-only tool dependency, imported inside the branch with a named error) or the runner's7zexecutable; a seeded sample of one chunk; a unit test with a tiny.7zbuilt in the test. - 3.3.3 Thingi10K hashes and first numbers: take
tet-corpus-hashes.jsonfrom the scheduled artifact into the manifest'smodels, and add a## Measured on Thingi10Ksection to tetrahedralize, which has no place for them (its Measured section is at line 45), filled fromtet_corpus.py summary, with a freshness test liketest_the_published_table_is_fresh. - 3.3.4 What the CAD corpus still fails on is its measure for the mesher, not a task of this item: 399 of 16,503 faces at a size of the diagonal over 40 (degenerate patches 270, trims leaving their patch 69, other 60), all on KR600, the RC buggy and the Ventilator.
- 3.3.1 NIST's MBE PMI test models (a US-government work): add an
- Done when. Both manifests carry hashes for every model and the two pages their tables.
3.4 Code cleaning, ordered by SonarCloud's report
The master scan (static analysis) ranks files by duplicated-lines density; an item closes when its copies call the shared kernels, whose history is in shared geometry and FEM kernels. The copies that need no kernel are §6. M–L
- The shared targets (all core-private under
src/cpp/src/detail/unless noted):math_utils.hpp(MathUtils::Sub,Add,Scale,Dot,Cross,Norm,Distance,UnitByDivisionwith a zero guard,AngleDeg,SolveSymmetric3);geometry_utils.hpp(TriangleNormal,TriangleTwiceArea,TriangleArea,TriangleAnglesDeg,SignedTwiceArea2D);bounding_box_utils.hpp(BoundingBox3,Expand,FromPointsseeded with the first point,FromCoords,Extent,Diagonal); the installedinclude/meshioplusplus/detail/geometry.hpp(Vec3,vec3_*,triple_product,det3);fem_kernels.hpp(tet_volume6,tet_volume,det_unit3,corner_jacobian_ratio,kHexCornerEdges,kWedgeCornerEdges); the installeddetail/cell_faces.hpp,cell_edges.hpp(surface types only) andcell_subdivision.hpp(cell_refine_edges, which holds the volume edges);bspline_ndu.hpp;always_inline.hpp(MIO_ALWAYS_INLINE). - Probe for every sub-item.
tools/dup_report.py --lang cpp --json out.jsonshows the file's duplicated lines dropping;tools/bench_ops.shwith--hashand aBASELINEreports identical digests;python3 tools/bench_instructions.py --check benchmark/instructions_S.csvpasses. A copy whose arithmetic differs from the kernel changes the last bits of its results, so it moves only when that change is shown not to happen or is intended and recorded. Each new or extended utility gets itstests/cpp/test_*_utils.cppcase and its row in common utilities. - 3.4.1 The remaining vector arithmetic. M
- Hand-written cross products in about 30 sources, for
MathUtils::Cross:operations/mesh_cad.cpp(lines 510, 2192, 2207, 3046, 3051, 4575, 5660, 6435),detail/step_brep.cpp(418, 1047, 1222, 1300, 1314, 1372),operations/fit_cad.cpp(99, 379, 555),tri_tri.cpp,tet_poly.cpp,render.cpp,nastran_model.cpp. Find the rest with a regex ona[1]*b[2] - a[2]*b[1]. - Unit vectors without a zero guard, which the shared guard would change on a zero vector:
coord_unit(operations/coordinates.cppline 45),pf_normalize(detail/patch_flatten.cppline 37). Decide per site whether a zero input can occur; if not, share and assert. - Lambdas:
crossandunitinrad_skew_axes(formats/radioss.cpplines 259–260),dotinbspline_lsq.cpp(347) andedge_sampler.cpp(145). - Bounding boxes seeded with ±infinity or ±1e300: about 50 loops in 32 files (
brep_nurbs.cpp318,external_mesher.cpp252,fit_cad.cpp299 and 402,reorder.cpp226,stats.cpp107,measure.cpp184,tetrahedralize.cpp478, six infeature_fit.cpp). They differ fromBoundingBoxUtils::FromPointson NaN and on an empty set: add aFromPointsOrEmpty(infinity-seeded) beside it rather than change either behaviour. - 31 local aliases of
std::array<double, 3>(QrV3,PatchVec,SfVec,LsV3,MbVec,CoordVec,UtgVec,TetV3,GeoVec,MphVec, …), forVec3: mechanical, one commit, no digest can move.
- Hand-written cross products in about 30 sources, for
- 3.4.2 Volumes and scaled Jacobians. M
- Hand-expanded determinants whose expansion order differs from
triple_product:utg_measureandutg_scaled_jacobian(operations/untangle.cpplines 65 and 99),ls_corner(detail/layer_sweep.cpp43),tp_signed_volume(operations/tet_poly.cpp181),rvol_tet_ok(operations/remesh_volume.cpp397). Each either moves with a recorded digest change or stays, listed in 3.4.11. - Quad scaled Jacobians:
quad_scaled_jacobian(quad_recombine.cpp67),quality_quad(quality.cpp187, the ratio at 224): oneGeometryUtils::QuadScaledJacobian. - Closed-surface and polyhedral volumes outside
poly_measure:hds_hex_volume(hex_dual_schemes.cpp1380),closed_surface_volume(detail/mesh_boolean.cpp550),repair_signed_volume(repair.cpp425),quality_facefan_volume(quality.cpp126), and the loops atoctree_mesh.cpp128,hull.cpp543,conservative_interpolate.cpp391 andproviders/occt/fit_cad_occt.cpp2696: oneGeometryUtils::ClosedSurfaceVolume(points, triangles)(9.4 needs the same integral's higher moments). kUtgHexCorner(untangle.cpp51) repeatskHexCornerEdgeswith the corner prepended.
- Hand-expanded determinants whose expansion order differs from
- 3.4.3 Shape functions and quadrature. Option A of the design. L
- Today:
ts_lagrange1d,ts_corner_weights,ts_shape_functions(operations/tessellate.cpplines 174–205, five quadratic types, the serendipity flag at 72);ct_shapeandcurved_triangle_jacobian(detail/curved_triangle.cpp40 and 231);cq_basis,cq_basis_d(detail/curved_quad.cpp33 and 40);mf_serendipity_shape(formats/mfem.cpp2618); and the general layer that already exists,formats/lagrange_common.hpp(namespacelagrange:basis,interpolation_matrixat line 721,fixed_lagrangeto order 10, the orthogonal Legendre and Dubiner bases at 629–649), with its twin_lagrange.py. - Step 1: move
lagrange_common.hppfromformats/todetail/reference_element.hpp(it is not format code; 9.5 needs it from an operation) and add first derivatives. Step 2: the four special-case bases call it, each behind the digest check. Step 3: quadrature in onedetail/quadrature.hpp: Gauss–Legendregauss_legendre(N, …)(detail/bspline_lsq.cpp192) and the 5-point table (detail/edge_sampler.cpp43–46) move there; add the simplex and tensor rules for triangle, quad, tet, hex, wedge and pyramid to the order the readers' integration-point data uses (8.3 names its rules from this table).
- Today:
- 3.4.4 Simplex FEM pieces. M
- P1 stiffness:
mph_simplex_stiffness(operations/morph.cpp183) andsobo_assemble(operations/sobolev_deform.cpp184), tofem_kernels.hpp. - Cotangent weights:
detail/surface_fem.cpplines 83–119 andcurv_corner/curv_dual_area(operations/curvature.cpp60 and 89). - The per-triangle gradient inline in
geodesic_distance(operations/geodesic.cpp223–237). - Barycentric coordinates:
interp_tet_containsandinterp_tri_contains(interpolate.cpp169 and 209),if_barycentric(interfaces.cpp385),Locate(streamlines.cpp308),McadDisc::Bary(mesh_cad_reparam.cpp156),rst_prepare_tri(raster.cpp106): oneGeometryUtils::TetBarycentric/TriangleBarycentricwith the containment tolerance a parameter, since the sites differ in it (9.3's locator is the sixth user). - Tet dihedral angles:
tet_dihedral_angles(detail/bcc_octree_tets.cpp105) is already whatoctree_mesh_optimizecalls;tet_dihedrals(tetrahedralize.cpp61),optvol_dihedrals(optimize_volume.cpp942) andquality.cpp348–350 remain.optvol_min_dihedral(438) stays: it is the measured hot loop of 5.5.2. - 3×3 solves:
gradientalready callsSolveSymmetric3; onlyquality_solve3(quality.cpp265, Cramer's rule, non-symmetric) remains, for aMathUtils::Solve3.metric.cppandtransform.cpphave none.
- P1 stiffness:
- 3.4.5 Topology tables. Read from
cell_facesandcell_refine_edgesinstead of typing them again, where the order agrees. S–M- Same order as the shared table, so mechanical:
kOctmtFacet(octree_mesh_tetra.cpp55),kOctmoFace(octree_mesh_optimize.cpp54),kFaces(conservative_interpolate.cpp281),kE(cut.cpp310). - A different order, so the loop's iteration order and with it tie-breaks and digests would change:
kFaces(tetrahedralize.cpp62),fverts(quality.cpp349), the triangulated faces ofboundary_layer.cpp135–142, the tet edges{0,1},{0,2},{0,3},…ofoptimize_volume.cpp(310, 442, 584, 945),kTetEdgeCorners(decimate_volume.cpp311) andkTetEdge(detail/marching.cpp57), andhex_dual_schemes.cpp's lexicographickHdsFaces/kHdsEdges(121–123). For these add the order as a named second table incell_subdivision.hpp(tet_edges_lexicographic) and share that; do not reorder.
- Same order as the shared table, so mechanical:
- 3.4.6 B-spline bases. Mostly closed:
nurbs_find_spanandnurbs_basis(detail/nurbs_eval.hpp45 and 73) are whatbspline_curve.cpp,bspline_lsq.cppandiga_kernels.hppcall.bn_spanandbn_basis(detail/brep_nurbs.cpp42 and 60) remain and behave differently (no repeated-knot skip; a zero-denominator guardnurbs_basislacks): decide which behaviour is right on a knot of full multiplicity, test it, then share.nurbs_basisitself does not usebspline_ndu. S - 3.4.7 A geometry abstraction, decided by use. The design note recommends kernels over a Kratos-style class hierarchy, and a non-virtual
GeometryViewover them when call sites want it; reopen the hierarchy only for an operation that caches integration-point data across many evaluations (8.3 may be it). Design pass, no code until an operation asks. S - 3.4.8 Format code that drifted apart. The Nastran writers share
detail/nastran_write_common; their card tables and unique-id rules differ by design. What remains:_read_single_mesh(src/python/meshioplusplus/med/_medmulti.pylines 373–480) repeatsreadofmed/_med.py(line 375): points (397), the FAS lookup (402–413), cells including thePOGblock (415–435). Extract_read_mesh_group(mesh_group, fas)in_med.py, called by both. S - 3.4.9 Binding and server boilerplate. Mostly closed:
_forward(mcp/_server.pyline 54) wraps 193 of the 201 tools. Nine keep hand-written bodies because they transform an argument first:sequence(362),render_mesh(2347),convert_coordinates(3049),estimate_normals(3271),extrude(3728),revolve(3766),mirror(3803),remesh(4240),screenshot(5089): leave them. The C API has 161fail(MIO_ERR_INVALID_ARG, …)null checks (bindings/c/c_api.cpp,failat line 426); acapi_require_arg(ptr, "name")macro returning the status would share them without changing a code. S - 3.4.11 Copies kept for the instruction gate. Shares measured and undone, to reopen only with a form that leaves the callers' code generation alone, measured with
tools/bench_instructions.py: the point-field validation ofgradient(gradient.cpplines 502–557) andhessian(hessian.cpp54–85), where one call in place of the inline throws costgradient_gg0.7% andhessian1.2%; one sign-and-digits scan under the four integer parsers ofdetail/text_cursor.hpp(strtoll_token351,strtoull_token380,ExtractInt,ExtractUnsigned:read_ugrid+8.1%,read_gmsh22_ascii+6.2%, though three readers ran 5 to 6% faster); andGeometryUtils::TriangleAreaforquality'stri_arealambda (quality.cpp325, +1.8%). Already reopened withMIO_ALWAYS_INLINEand shared:detail/staged_block.hpp,ccells_finish_identity(convert_cells.cpp379),detail/subset_gather.hpp. First thing to try on the rest: the same macro. S - 3.4.12 Behaviour the copies disagreed on. Decide each, then make the copies agree, with a test that pins the decision. S
time_valueson a file object:nastran_op2/__init__.py(line 41),radioss_th(37),marc(65),ansys_rst(72) andlsdyna_d3plot(40) call the core unguarded;femap(37) andabaqus_fil(36) testis_bufferfirst. Recommended: guard all, through one helper in_shims.py._medmulti.pyline 303 passeswrite_num=Falseto_write_mesh_step(_med.py742), somed:numis never written by the multi-mesh writer. Recommended: write it._clean.py(259) and_crop.py(331) call_set_remap.remap_sets, which raises on an out-of-range set index;_decimate.py(652–673) filters and drops it. Recommended: raise everywhere; a set index out of range is a broken input.- Medit
Tangents/TangentAtVertices: the core skipsntokens (formats/medit.cpp570–579), the twinnlines with a warning (medit/_medit.py497–505). Closes with 2.3.9.5, which reads them.
3.5 Clang-Tidy beyond includes
Grow the checked set in stages, each first an advisory report and then a gate on new findings. M–L
- State today.
.clang-tidyisChecks: '-*,misc-include-cleaner'with aHeaderFilterRegexoversrc/cpp/(include|src)|bindings/cand no naming options.tools/performance_tidy.py(307 lines, behindtools/performance-tidy.sh) configuresbuild/performance-tidy-<backend>with Clang,CMAKE_EXPORT_COMPILE_COMMANDS=ON, SEQ, the C API and CLI on and Python off, runs the checks per translation unit and writes deduplicated JSON (schema_version1); butCHECKS = "-*,performance-*"is hard-coded at line 17 and the check-list regex at line 239 matchesperformance-only. No baseline file is committed: the counts live in benchmarks (520, 283 and 284 on MESHIO, NATIVE and KRATOS after the first batch).tools/include-cleanup.shand theinclude-cleanupjob (ci.ymllines 1360–1383) are the gate pattern.clang-tidy-diffships with the runner's clang-tidy. - Steps.
- 3.5.1 A general runner. Rename to
tools/tidy_report.pywith--checksand--baseline FILE(compare by file, check and message, not line), keepperformance-tidy.shas a wrapper, and commit a baseline per family undertools/tidy/. Test intests/python/besidetest_check_abi_version.py. - 3.5.2
bugprone-*first (the family most likely to find defects), thencert-*andmisc-*beyond includes: report, triage into fixed,NOLINT(<check>): <reason>and disabled-with-reason in.clang-tidy, then gate. - 3.5.3 A chosen subset of
cppcoreguidelines-*(init-variables,slicing,narrowing-conversionson the readers,pro-type-member-init); not the ownership or bounds families, which fight theNDArrayviews. - 3.5.4
modernize-*andreadability-identifier-namingconfigured for the house style (CamelCasetypes and methods,mprefix members,r/pprefix parameters,lower_casefree functions and locals), with the Python and JS API string names untouched. - 3.5.5 The gate. A
tidy-diffstep afterinclude-cleanup:git diff -U0 origin/master... | clang-tidy-diff -p1 -path <build>over the enabled set, failing on any finding in changed lines; the full report runs onmasterbeside SonarCloud. - 3.5.6 Triage the overlap with SonarCloud's C++ rules and 3.10 once (a table in static analysis: rule family, owning tool), and close the
performance-*inventory.
- 3.5.1 A general runner. Rename to
- ABI. A fix in an installed header follows the ABI policy: an inline-body change is Tier B.
- Done when.
.clang-tidyenables the four families' chosen checks and the diff gate is a required check.
3.6 Compiler warnings
The C++ build sets no warning flag on any compiler, so what every compiler finds for free is never seen. M–L
- State today. No
-Wall,-Wextra,/W4orCOMPILE_WARNING_AS_ERRORinCMakeLists.txtorcmake/; the only-Wall -Wextraisbindings/octave/meshioplusplus/tools/dev_build.shline 39. The core's flags accumulate in_mio_core_copts(lines 746–752) and are applied PUBLIC bymeshioplusplus_configure_core_target()(lines 1412–1455), so they reach the exportedINTERFACE_COMPILE_OPTIONSof the installed targets:MESHIOPLUSPLUS_COVERAGE(819–827) andMESHIOPLUSPLUS_SANITIZE(843–879) use that list on purpose, a warnings option must not. Third-party code compiled into the core's own source list:pugixml.cpp(line 708) and the gidpost C sources (716); the Eigen, JSON and Polyscope submodules are undersrc/cpp/third_party/. - Steps.
- 3.6.1 The option.
option(MESHIOPLUSPLUS_WARNINGS "…" OFF)andMESHIOPLUSPLUS_WARNINGS_AS_ERRORS, with a functionmeshioplusplus_target_warnings(target)that adds the flagsPRIVATE(-Wall -Wextraon GNU and Clang,/W4on MSVC) and is called formeshioplusplus_core_obj, eachmeshioplusplus_core_<backend>,_core,meshioplusplus_c_obj,meshioplusplus_cli,meshioplusplus_testsand the benchmarks. Exempt the vendored sources withset_source_files_properties(... PROPERTIES COMPILE_OPTIONS -w)and mark third-party include directoriesSYSTEM(the_mio_core_sys_incslist exists). A--warningsswitch inbuild/configure.shand.bat. A test in the consumer project asserts the installed target carries no-Woption. - 3.6.2 The count. One advisory CI step per compiler that builds with the option and uploads the log; record the three counts here.
- 3.6.3 To zero, then
-Werror. Fix by family, readers first; thenMESHIOPLUSPLUS_WARNINGS_AS_ERRORS=ONin onecpp-testsleg (GCC),cpp-sanitize(Clang) and one Windows leg. - 3.6.4 The warnings that find defects in parsing code, one at a time, each to zero before the next:
-Wshadow,-Wconversionand-Wsign-conversiononsrc/cpp/src/formats/anddetail/parse_guard.hppfirst (a narrowed header count is how a reader overflows),-Wold-style-cast,-Wnull-dereference,-Wimplicit-fallthrough,-Wformat=2. - 3.6.5 Warning-free headers for consumers.
tests/consumerand theamalgamationjob's smoke compile build with-Wall -Wextra -Werror(and/W4 /WX), so a user's strict build does not break on meshio++'s installed headers or the single header.
- 3.6.1 The option.
- Done when. The three
-Werrorlegs are green and the consumer test compiles the headers at that level.
3.7 Typed Python
A user's mypy or pyright sees every call as Any, and an editor completes nothing from the extension. M–L
- State today. No
py.typed, no.pyi, no type checker inpyproject.toml,tox.ini,.pre-commit-config.yamlor CI. Annotated definitions are few (_helpers.py19 of 28,_mesh.py7 of 22).__all__is a static list (src/python/meshioplusplus/__init__.pyline 631, about 357 names) resolved lazily through_LAZY_ATTRS(376) and a module__getattr__(992–1013), with the imports mirrored underTYPE_CHECKING, so a checker can follow them._coreis one pybind11 module (bindings/python/_core.cpp, 10,482 lines). The MCP tool functions (mcp/_tools.py) have no annotations; the typed wrappers are inmcp/_server.py. - Steps.
- 3.7.1 Stubs for
_core. Generatesrc/python/meshioplusplus/_core.pyiwith pybind11-stubgen from a built extension (tools/gen_core_stubs.sh), commit it, and add a freshness check to thetestjob in the manner oftools/amalgamate.sh --check; fix the pybind signatures that render as raw C++ types (missingpy::argnames, unregistered types) until stubgen reports none. - 3.7.2
py.typed. An empty marker in the package (it ships becausewheel.packagescovers the directory; assert it intests/wheel_base_smoke.py). - 3.7.3 A checker, staged.
[tool.mypy]inpyproject.toml(or pyright; pick one), atypechecktox environment and CI job: first__init__.py,_mesh.py,_helpers.py,_fallback.py,_shims.pyunder--strict; then the operation shims (_<op>.py); then the CLI and MCP packages; the format twins last and non-strict. Each stage advisory, then required. - 3.7.4 A typed-usage test.
tests/python/typing/usage.py(a script that reads, cleans and writes) checked withmypy --strictin the job: the probe that a user's code type-checks. - 3.7.5 Ruff, measured. Replace isort, black and flake8 in
.pre-commit-config.yaml,tox.iniand thejustfilewith Ruff only if its formatter leaves the tree unchanged or the one-commit diff is reviewable (record it in.git-blame-ignore-revs); addB,UP,SIM,PERFandSadvisory first. Whether or not Ruff lands, fix the hook:.flake8selectsBandB9, but the flake8 hook has noadditional_dependencies: [flake8-bugbear], so those checks have never run (nor in tox'slint).
- 3.7.1 Stubs for
- Done when.
mypy --strict tests/python/typing/usage.pypasses against an installed wheel.
3.8 clang-format as a check
- State today.
.clang-formatexists (BasedOnStyle: Google,IndentWidth: 4,ColumnLimit: 100,SortIncludes: Never) andAGENTS.mdasks for it; no hook or job runs it. - Steps. (1) Measure:
git ls-files 'src/cpp/**' 'bindings/**/*.{cpp,hpp,h,c}' | xargs clang-format --dry-runat one pinned version, excludingsrc/cpp/third_party, the single header and the generated tables; record the changed-line count. (2) If small: reformat in one commit listed in.git-blame-ignore-revs, regenerate the single header, confirmtools/bench_ops.sh --hashis unchanged (formatting cannot move a digest; the check is that nothing else slipped in). If large: skip the reformat. (3) Either way, gate changed lines: apre-commithook (pre-commit/mirrors-clang-format, pinned) and alintstep runninggit clang-format --diff origin/master. - Done when. A pull request with a mis-indented line fails
lint. S
3.9 More sanitizers and hardened runs
- State today.
cpp-sanitize(ci.ymlline 238) runs ASan and UBSan with Clang on the SEQ backend without Python.MESHIOPLUSPLUS_SANITIZEtakes any list, but CMake stops with a fatal error when it is combined with Emscripten orMESHIOPLUSPLUS_BUILD_PYTHON(CMakeLists.txtlines 868–871), andbuild/configure.shforces Python off (lines 177–180). The parallel backends are ininclude/meshioplusplus/parallel.hpp; the THREADS pool, new in v19.0.0, issrc/cpp/src/parallel_threads.cpp. Parallel tests: suitesParallel,ParallelThreads,LibraryLock, andtests/cpp/thread_sweep.hppin 20 test files. - Steps.
- 3.9.1 ThreadSanitizer. A
cpp-tsanjob: Clang,-DMESHIOPLUSPLUS_SANITIZE=thread,-DMESHIOPLUSPLUS_PARALLEL_BACKEND=THREADS, thenTBB, tests withMESHIOPLUSPLUS_NUM_THREADS=4; OpenMP needs an instrumented libomp (thelibompbuilt with-DLIBOMP_TSAN_SUPPORT=ON, or archer) and comes last. The determinism job compares outputs, which a benign-looking race passes; this is the first race detector on the pool. Suppressions intools/sanitizers/tsan.supp, each with its reason. M - 3.9.2 The Python extension under ASan. Lift the CMake guard behind an explicit
MESHIOPLUSPLUS_SANITIZE_PYTHON=ON; a job that builds_corewith ASan and runstests/python(the fast subset) withLD_PRELOAD=$(clang -print-file-name=libclang_rt.asan-x86_64.so)andASAN_OPTIONS=detect_leaks=0. Target:bindings/python/np_conversions.hpp, where capsule-backed arrays and borrowed views cross the language boundary and no sanitizer has looked. M - 3.9.3 Hardened standard libraries. One
cpp-testsleg with-D_GLIBCXX_ASSERTIONSand one Clang leg with-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVEand-stdlib=libc++: an out-of-rangeoperator[]becomes an abort. Through aMESHIOPLUSPLUS_HARDENED_STLoption appliedPRIVATElike 3.6's. S - 3.9.4 MemorySanitizer on
meshioplusplus_fuzz_readfor the formats that need no external library (everything outsidetools/fuzz/library_formats.txt), with an MSan-instrumented libc++; advisory. M
- 3.9.1 ThreadSanitizer. A
- Done when.
cpp-tsanis a required check on THREADS and TBB.
3.10 More analyzers, triaged once
Nothing analyses a pull request before it merges: SonarCloud scans master only, with continue-on-error. M
- Steps.
- 3.10.1 CodeQL.
.github/workflows/codeql.ymlon pull requests and weekly:cpp(build modemanual, thecpp-testsconfigure line with SEQ and Python off),python,javascript-typescript(src/viewer,src/wasm) andactions. Triage the first run; queries closed by design go in acodeql-config.ymlwith reasons, mirroringsonar.issue.ignore.multicriteria. Its taint tracking covers what Sonar flagged by hand: the MCP server's_resolve(mcp/_tools.pyline 325) and the HTTP companion. - 3.10.2 GCC
-fanalyzerand Clang's analyzer, advisory:-fanalyzeronbindings/c/c_api.cppandsrc/cpp/src/formats/in one scheduled job (it is slow and noisy on C++; keep it off pull requests), and theclang-analyzer-*checks through 3.5.1's runner. - 3.10.3 Thread-safety annotations. A
detail/thread_annotations.hppwith the Clang capability macros (empty elsewhere) and-Wthread-safetyin the Clang legs, on the mutexes the core owns:library_mutex()(src/cpp/src/detail/library_lock.cpplines 26–33, withLibraryLock/LibraryUnlock),ProvidersState::mMutexandplugin_scan_mutex()(src/cpp/src/providers.cpplines 88 and 294), the pool'smSubmit,mMutexandgPthrCreate(parallel_threads.cpplines 193–206),buffer_allocator_mutex()(the installedndarray.hppline 78: a comment-only change there, no ABI tier),log_mutex(log.hpp177),mmg_mutex(),netgen_mutex()and the cache mutex offormats/nastran_op2.cpp(4302). This makes "no two threads inside the libraries" (5.8.2) a compile-time property. - 3.10.4 One owner per rule family, decided with 3.5.6 and recorded in static analysis.
- 3.10.1 CodeQL.
- Done when. CodeQL is a required check with zero open alerts of severity high.
3.11 Coverage and test depth
- 3.11.1 Coverage on pull requests. The
coveragejob (ci.ymllines 1534–1745) runs only on pushes tomasterand tags (line 1537);.codecov.ymlsetspatch: falseand projectthreshold: 1%. Run it on pull requests (it uses ccache already, lines 1616–1620), setpatch: { default: { target: 80%, threshold: 5% } }, and keep thecoverage-reportsartifact SonarCloud consumes (7.2). S - 3.11.2 Hypothesis everywhere.
tests/python/test_properties.py(ten properties) isimportorskip-ed and Hypothesis is installed atci.ymlline 1611 only: add it to thetestjob's install line with a CI profile (derandomize=True, a deadline that fits the slowest runner). S - 3.11.3 A mutation-testing spike. On the shared kernels (
math_utils.hpp,geometry_utils.hpp,fem_kernels.hpp,detail/parse_guard.hpp) with Mull againstmeshioplusplus_tests, and on_helpers.pyand one format twin with mutmut: the surviving mutants name covered lines no assertion checks. Record the score and the survivors in static analysis, fix the tests, then decide whether a monthly job is worth its minutes. S–M - 3.11.4 Examples that run. No job executes the notebooks (72 in
example/python, 11 inexample/cpp, 4 each for Julia, R and Octave): a weeklynotebooksjob runsPYVISTA_OFF_SCREEN=true jupyter nbconvert --to notebook --executeon the Python ones into a scratch directory and fails on an exception (outputs are not compared). And pytest-codeblocks coverstests/python/only (ci.ymllines 1513 and 1662): addpytest --codeblocks README.md doc/to thetestjob, marking the blocks that need optional packages or files with<!--pytest-codeblocks:skip-->. S–M
3.12 Fuzzing beyond readers
After 3.1, so new targets run where the old ones do. M
- State today.
tests/fuzz/fuzz_read.cppreads every registry format (through a scratch file: the registry is path-only,registry.hpphas no in-memory read or write);fuzz_mesh_cad.cppholds the two operation targets, dispatched byMIO_FUZZ_TARGETor the binary name and reporting throughfuzz_op_finding;hdf5_mutator.cppis the structure-aware HDF5 mutator. Targets are defined atCMakeLists.txtlines 2144–2192 and regressions becomefuzz_regression_<dir>tests at 2193–2219 (op_*directories replay throughmeshioplusplus_fuzz_ops_replay).fuzz.ymlruns weekly and on pull requests that touch the fuzz paths. - Steps.
- 3.12.1 A round-trip target in
tests/fuzz/fuzz_roundtrip.cpp: read the input as format F; if it reads, write it to a scratch path withregistry_writers().at(F), read that, and callfuzz_op_findingwhen the second read throws or differs from the first beyond the format's declared losses (compare counts, cell types and array names and shapes first; values only for the formatsconformance_spec.pydeclares exact). Finds writer crashes and reader/writer disagreements no reader target can. - 3.12.2 A differential replay, not a libFuzzer target:
tools/fuzz/diff_engines.pyreplays each text format's corpus through_core(MESHIOPLUSPLUS_STRICT_CORE=1) and the Python twin and reports inputs where one reads and the other raises, or the meshes differ. Weekly, infuzz.yml, after the campaign, on its corpus. - 3.12.3 The C API as
tests/fuzz/fuzz_c_api.cpp: a byte-driven sequence ofmio_mesh_*builders, getters after mutation (borrow windows), option structs with random reserved tails, andmio_readon the remaining bytes; any crash or a non-status failure is a finding. - 3.12.4 Operations on untrusted meshes:
clean,repair,convert_cells,decimate,extract_surface,tetrahedralize's native backend, each taking the mesh from the reader target's corpus for a compact format (off,medit) so the fuzzer mutates meshes, with the allowed exceptions listed per operation asfuzz_mesh_cad.cppdoes. - 3.12.5 Parsers of text the user writes: the pipeline JSON (
pipe_document_from_json) and the native CLI's argument vector. - Register each in
CMakeLists.txt,tools/fuzz/run_campaign.sh, the OSS-Fuzzbuild.shand fuzzing.
- 3.12.1 A round-trip target in
- Done when. The weekly campaign runs the new targets and their first findings are fixed with regressions under
tests/fuzz/regressions/.
3.13 A mechanical ABI and API diff
- State today.
tools/check-abi-version.shfails when an installed header changed since the last tag without a version bump or an ABI review row naming it;tests/cpp/test_abi_layout.cpppins 195 types, on Linux x86-64 with libstdc++ only; a link-time sentinel covers version mismatch. Nothing sees an exported symbol that changed or disappeared, or a type nobody listed. The shared libraries aremeshioplusplus(C API,SOVERSION 0,CMakeLists.txtlines 1928–1936) andmeshioplusplus_core_<backend>(SOVERSION= the ABI version, shared withBUILD_SHARED_LIBSorMESHIOPLUSPLUS_PYTHON_SHARED_CORE). - Steps.
- 3.13.1
abidiffon the C API, gating. Incpp-install: build the C library at the last release tag and at head with-g, run libabigail'sabidiff --headers-dir1/2 bindings/c/include; any removed or changed symbol or struct layout fails unless the major version changed. The C promise is the strict one, so this is a gate from day one. S - 3.13.2
abidiffon the shared core, advisory, with a suppression file fordetail::and with the report attached to the pull request: a changed type with no ABI bump is what the review row should have caught. S - 3.13.3 A Python API diff. A committed
tools/api/python_api.json(each name of__all__with itsinspect.signatureand defaults, generated bytools/gen_api_dump.py), a freshness test, and a rule inlint: a removed name or a changed default in the diff needs a**Breaking:**line inCHANGELOG.md. The MCP parity test can read the same file. S–M
- 3.13.1
- Done when. Removing a C function in a test branch fails
cpp-install.
3.14 Supply chain
Publishing already uses OIDC for PyPI and (with a token fallback, wasm.yml lines 278–307) npm, and downloads are hash-pinned (build/deps-pins.sh). Each sub-item is S.
- 3.14.1
SECURITY.mdat the root: supported versions (the last minor), private reporting through GitHub's security advisories, what is in scope (readers on untrusted files, the MCP server's sandbox, the HTTP companion), the response target. Link it fromREADME.mdand fuzzing. Before 3.1.4. - 3.14.2 Dependency updates.
.github/dependabot.ymlforgithub-actions,npminsrc/viewer,src/wasmanddoc, andpipfor the pinned tools; grouped weekly. The C libraries ofbuild/deps-pins.share not covered by it: a scheduled job that compares each pin with its upstream's latest release and opens an issue. - 3.14.3 Actions pinned by commit. Every
uses:is a tag, andpypa/gh-action-pypi-publish@release/v1a branch: pin each to a SHA with the tag in a comment (Dependabot keeps them current), and addpermissions: contents: readat the top of each workflow with per-job elevation. - 3.14.4 OpenSSF Scorecard workflow with the badge in
README.md; it then tracks the rest. - 3.14.5 Provenance.
actions/attest-build-provenancefor the wheels and sdist (wheels.yml), the CLI archives (cli.yml), the WASM tarball (wasm.yml) and the Conan and vcpkg artifacts; verification (gh attestation verify) documented in installation. Drop npm's token fallback once OIDC has published a release. - 3.14.6 An SBOM. CycloneDX JSON per wheel and per CLI archive, generated from
build/deps-pins.sh,build/occt-recipe.shand the CMake cache (atools/gen_sbom.py), naming the statically linked HDF5, netCDF, zlib, zstd, LZ4, bzip2, CGNS, KaHIP, CutCells and OpenCASCADE versions; a test thatTHIRD_PARTY_NOTICES.md's version column agrees with the pins, which today is kept by hand. - 3.14.7 Licence identifiers. 34 sources carry
SPDX-License-Identifier: MIT; the rest an ASCII banner. Add the identifier to the banner everywhere (a scripted, comment-only change; regenerate the single header) and aREUSE.tomlcovering fixtures and generated files, withreuse lintinlint.
3.15 The platform matrix
- State today.
ubuntu-24.04's GCC 13, an unversioned apt Clang, Apple Clang, MSVC; gcc-toolset-13 in the manylinux_2_34 images; CPython 3.10 to 3.14 with the GIL;CIBW_SKIP: "*-musllinux* *_i686 *-win32"(wheels.ymllines 108–109). No minimum compiler is stated anywhere but the Spack recipe'sconflictsbelow GCC 10; wheel comments say<format>needs GCC 13. - Steps.
- 3.15.1 Free-threaded CPython. (a) Raise the pybind11 floor from
>=2.11(pyproject.tomlline 2) to a version withpy::mod_gil_not_used(2.13 or later) and replace the hand-rolled once-caches the comment atbindings/python/np_conversions.hppline 172 explains. (b) Audit the state the bindings share: the leaked class cachessMesh,sRegion,sRows,sFaces(np_conversions.hpplines 177–200, set "under the GIL"),_ALL_LOADEDand the three_Registrydicts in_helpers.py(lines 61 and 113–115),_strictin_fallback.py, the module__getattr__writingglobals(), the unlocked caches innastran_op2/_write.py(1795),nastran_h5/_write.py(98) and_tessellation.py(907),_LIBinszplt/_szplt.py. (c) Declare the module (PYBIND11_MODULE(_core, m, py::mod_gil_not_used()),_core.cppline 1998) only when 3.9.1's ThreadSanitizer run of a threaded pytest subset is clean. (d) Addcp313t-* cp314t-*toCIBW_BUILD(the patternscp313-*do not match them) and a3.14tentry to thetestmatrix. Without the wheel a free-threaded interpreter re-enables the GIL on import, with a warning. M–L - 3.15.2 musllinux. Drop
*-musllinux*fromCIBW_SKIPfor the base wheel;build/build-permissive-libs.shmust build under musl (HDF5 and netCDF do);-static-libstdc++works there. Add-ons after the base. S–M - 3.15.3 Big-endian. A weekly
s390xjob under QEMU (uraimo/run-on-arch-actionor a--platform linux/s390xcontainer): configure with SEQ, HDF5 and netCDF off, runmeshioplusplus_tests. Every binary reader and writer states a byte order throughdetail/byteswap.hpp; nothing has run them where the host's differs. Expect findings: list them in §1 as they appear. S–M - 3.15.4 Stated compilers. Decide the floor by building: GCC 11, 12, 13 and Clang 15 to 17 in a
compiler-matrixjob (containers); state the lowest that passes in installation andREADME.md, keep that leg required, and add the newest GCC and Clang as an advisory leg. S - 3.15.5 PyPy. Not queued: pybind11 supports it, no user has asked.
- 3.15.1 Free-threaded CPython. (a) Raise the pybind11 floor from
- Done when.
pip install meshioplusplusonpython3.14tand on Alpine installs a wheel, and the s390x job is green.
3.16 Documentation checks
- 3.16.1 Links.
lycheeoverdoc/.vitepress/distandREADME.md: internal links and anchors on pull requests (indocs.yml, which has no check today), external ones weekly with an allow-list for vendor portals that block bots. S - 3.16.2 This file checks itself.
tests/python/test_roadmap.py, besidetest_doc_diagrams.py: everyN.MorN.M.Kcited in running text names a heading or a bold item that exists (the November 2026 passes found ten left behind by renumberings); every item in Suggested sequencing and every box ofroadmap_map()(doc/diagrams/figures/flows.py) is open; everypathin backticks that looks like a repository path exists. The same test should cover the references to roadmap numbers in code and other pages, which are stale today and to be corrected with it:doc/benchmarks.md(lines 931, 1007, 1257, 1271),src/cpp/src/detail/always_inline.hpp,src/cpp/src/detail/library_lock.hpp,src/cpp/src/operations/optimize_volume.cpp,src/cpp/benchmark/bench_ops.cpp(line 1066) and.github/workflows/wheels.yml(line 139) cite numbers of earlier layouts; replace each number with the item's anchor or title. S - 3.16.3 Spelling.
codespellin pre-commit with a project word list. S - 3.16.4 Claims the tree does not back. Fuzzing line 3 lists a VS Code extension among the consumers of untrusted files; only
.vscode/tasks.jsonexists (remove it, or build 4.13). MCP says 126 tools whereTOOL_REGISTRYholds 203: generate the count, as the conformance table is generated. The comment atCMakeLists.txtline 1619 says "All 112 headers" of 283. S
3.17 Build ergonomics
- 3.17.1 CMake presets. A
CMakePresets.json(schema 3, which CMake 3.21 reads; the project's floor of 3.15 is unaffected because presets are optional) with configure, build and test presets:dev(tests, STL backend),tests-<backend>,sanitize,fuzzers,coverage,lto,bench,cli,c-api,install-cpp,wasm(toolchain fromEMSDK).build/configure.shhas no switch for coverage, LTO, benchmarks or JSON, so the presets set those variables directly and the script gains--preset; CI's hand-written configure lines move to presets one job at a time. S–M - 3.17.2 A compiler cache in every job. ccache appears in
coverageonly (ci.ymllines 1616–1620,hendrikmuhs/ccache-action, launcher flags at 1632–1633). Add it tocpp-tests,cpp-sanitize,determinism,instructions,cpp-installand the optional-dependency jobs, keyed by job and compiler;sccacheon Windows. Not theinstructionsbuild if it changes counts (it cannot, but verify once). S - 3.17.3 Compile time.
-ftime-tracewith ClangBuildAnalyzer on a full build; report the ten slowest translation units (src/cpp/cli/main.cpp,bindings/python/_core.cppat 10,482 lines,bindings/c/c_api.cpp,bindings/wasm/js_bindings.cppat 11,083 lines) and headers in benchmarks. Then, by what it shows: split_core.cppandjs_bindings.cppby area into several sources (they are globbed, so no CMake change), precompiled headers formeshioplusplus_tests, and aCMAKE_UNITY_BUILD=ONleg as a check that the amalgamation's unique-name rule holds for the sources too. M - 3.17.4 Hardening the released binaries. For
cli.ymlandwheels.ymlbuilds only, through aMESHIOPLUSPLUS_HARDENINGoption appliedPRIVATE:-D_FORTIFY_SOURCE=3,-fstack-protector-strong,-Wl,-z,relro,-z,now,-fcf-protection=full(x86-64) or-mbranch-protection=standard(arm64),/guard:cfand/Qspectreon MSVC. Measure on the instruction-count gate first and record the cost. S–M
4. Ecosystem reach
Admission: getting what exists to the people who would use it.
4.1 Registration and distribution
Mostly logistics, and all blocking real adoption; the effort is small per target but the calendar time is set by each registry's review, so start early. Two rows are not logistics, and say so. S each, calendar-bound, except 4.1.2 and 4.1.3.
| Item | Target | Status today |
|---|---|---|
| 4.1.1 | conda-forge | No recipe exists. |
| 4.1.2 | CRAN | Not submittable as it stands: the package needs a C library CRAN's builders do not have. M |
| 4.1.3 | Julia General, with a JLL | Not registered; no binary package, so Pkg.add would give a package that cannot load its library. M |
| 4.1.4 | Blender Extensions Platform | Zips built and attached to every release; only the listing remains. |
| 4.1.5 | ConanCenter, the vcpkg registry | Recipes are CI-validated but in self-hosted shape, not upstream shape. |
| 4.1.6 | A ParaView plugin index | Bundles ship on every release and load in Kitware's binaries; only a listing remains. |
| 4.1.7 | Homebrew, Nix | Not started. |
| 4.1.8 | A container image | Not started. |
| 4.1.9 | Octave Packages index, MATLAB File Exchange | Not started. |
Spack is upstream already (meshioplusplus and py-meshioplusplus in spack/spack-packages), and its bump is part of the release checklist in AGENTS.md.
- 4.1.1 conda-forge. A
staged-recipespull request with one recipe, two outputs (libmeshioplusplus: the C and C++ libraries and the native CLI;meshioplusplus: the Python package). Source: the PyPI sdist (a GitHub tarball lacks the Eigen, JSON and Polyscope submodules). Host dependencies as theparaview-pluginjob already installs them from conda-forge (ci.ymlnear lines 1900–1915):hdf5,libnetcdf,zlib,zstd,lz4-c,bzip2,cgns,kahip,pybind11,scikit-build-core,numpy, a C++20 compiler. CMake:-DMESHIOPLUSPLUS_PREFER_STATIC_DEPS=OFF, theWITH_*options on for what conda-forge has. OpenCASCADE is on by default (CMakeLists.txtline 435) and conda-forge hasocct: decide between an MIT base output with-DMESHIOPLUSPLUS_WITH_OCCT=OFFplus ameshioplusplus-occtoutput (the PyPI split, recommended) and one LGPL-linked package. macOS deployment target 13.3. Probe:conda buildlocally, thenconda create -n t meshioplusplus && python -c "import meshioplusplus". - 4.1.2 CRAN.
bindings/r/meshioplusplus/DESCRIPTIONsaysSystemRequirements: meshio++ C library (libmeshioplusplus), C99, andconfigurefinds an installed library (MESHIOPLUSPLUS_CFLAGS/LIBS,MESHIOPLUSPLUS_HOME, thenpkg-config) or exits;R CMD check --as-cranpasses in CI only because therjob builds the C API first (ci.ymllines 2640–2663). CRAN's machines have nolibmeshioplusplus, so the package would fail there. Options, to decide first: (a) vendor the core's sources into the package tarball (atools/make_cran_tarball.shcopyingsrc/cpp/include,src/cpp/srcandbindings/cundersrc/, aMakevarsthat compiles them withCXX_STD = CXX20, HDF5 and netCDF off unless found), within CRAN's 5 MB tarball limit and its compile-time limit, which the core's size may exceed: measure both before anything else; (b) aconfigurethat downloads a prebuilt static library per platform from the GitHub release, which CRAN tolerates for Windows and macOS binaries but not as the only path on Linux; (c) R-universe instead of CRAN, which builds from the repository with system requirements installable, and needs only apackages.json. Recommended: (c) now, (a) if the measurements allow. Also missing:cran-comments.md, acleanupscript,inst/COPYRIGHTSfor vendored code. Probe:R CMD check --as-cranin a container with no meshio++ installed. - 4.1.3 Julia.
bindings/julia/MeshioPlusPlus/Project.tomlhas[compat] julia = "1.9"only, andsrc/libmio.jlloadsENV["MESHIOPLUSPLUS_LIB"]or whateverLibdl.find_libraryfinds (lines 35–53); Julia says "deliberately no JLL". Steps: (1) abuild_tarballs.jlfor Yggdrasil building the C library (-DMESHIOPLUSPLUS_BUILD_C_API=ON, Python off, HDF5 and netCDF fromHDF5_jllandNetCDF_jll) for the platforms BinaryBuilder supports with C++20, givingmeshioplusplus_jll; (2)libmio.jlprefers the JLL'slibmeshioplusplusand keeps the environment override, withmeshioplusplus_jllin[deps]and[compat]; (3) register through Registrator withsubdir=bindings/julia/MeshioPlusPlusand TagBot for tags. The binding is GPL-3.0 (bindings/julia/LICENSE) while the library is MIT: confirm that is intended before the registration makes it permanent. Probe:Pkg.add("MeshioPlusPlus"); using MeshioPlusPlus; MeshioPlusPlus.version()in a clean depot. - 4.1.4 Blender.
src/blender/blender_manifest.toml.in(id meshioplusplus,license ["SPDX:MIT"],blender_version_min 4.2.0), built per platform bywheels.yml'sblender_extensionjob withcp311wheels. Submit at extensions.blender.org; the review asks for the bundled wheels' licences (the base wheel is MIT with static permissive libraries: point toLICENSE-BINARIES.md). Add the listing to Blender. - 4.1.5 ConanCenter and vcpkg. Conan: the real recipe is the root
conanfile.py, which usesexports_sourceswith local paths;packages/conan/is the test package. ConanCenter needsrecipes/meshioplusplus/all/{conanfile.py,conandata.yml,test_package/}withsource()callingget(**self.conan_data["sources"][version])on the release tarball and itssha256, and dependencies as ConanCenter requirements rather than vendored. vcpkg:packages/vcpkg/meshioplusplus/portfile.cmakehasSHA512 0andHEAD_REF main, and a private overlay portpackages/vcpkg/libaec/; upstream needs the real hash, aversions/entry (vcpkg x-add-version), and either the upstreamlibaecor a pull request fixing it there first. Both recipes force Eigen and JSON off, which is right for a registry build. Generate both upstream forms from the self-hosted ones in the release job, so they cannot drift. - 4.1.6 ParaView.
tools/paraview-meshioplusplus-plugin.pywith the bundles ofsrc/paraview/build_bundle.py. There is no central index; the listing is a page on the ParaView Discourse "Plugins" category and an entry in Kitware's community-plugin list, with the install lines of ParaView plugin. - 4.1.7 Homebrew, Nix. A formula in a tap first (
loumalouomega/homebrew-meshioplusplus: the native CLI and the C and C++ libraries,depends_on "hdf5","netcdf","cmake" => :build), homebrew-core once it has users; aflake.nixat the repository root exposing the CLI and the Python package, then nixpkgs. The release binaries cover the CLI meanwhile. - 4.1.8 A container image.
ghcr.io/loumalouomega/meshioplusplus:<version>: a slim Python base withpip install "meshioplusplus[all,addons]"and the native CLI archive, built and pushed by a job inwheels.ymlafter publishing, with 3.14.5's attestation; for CI pipelines and HPC (Apptainer pulls it). Only the OSS-Fuzz Dockerfile exists today. - 4.1.9 Octave and MATLAB.
pkg installtakes a tarball built by hand (Octave / MATLAB). Attachmeshioplusplus-octave-<version>.tar.gzto the release (a step inwheels.yml:tar -C bindings/octave -czf …), then a pull request tognu-octave/packageswith the package's YAML. MATLAB itself has never been run in CI: add amatlab-actions/setup-matlableg to theoctavejob running the same test directory before listing on File Exchange.
4.2 Rust bindings
Over the C API: the next language by scientific adoption after Julia and R. M
- State today. No
bindings/rust. The headerbindings/c/include/meshioplusplus/meshioplusplus.h(9,181 lines; 639 functions, about 163 structs, about 30 enums) is C99, includes onlystddef.handstdint.h, and CI compiles a-std=c99consumer. Conventions (header lines 25–40): a status orNULL/-1return with the thread-localmio_last_error(); setters copy; getters borrow until the next mutation; row-major, 0-based; string getters followsnprintf. Discovery:meshioplusplus.pcandfind_package(meshioplusplus). - Design.
meshioplusplus-sys:bindgenat build time from the installed header (pkg-configcrate;MESHIOPLUSPLUS_DIRoverride), with care for:MIO_APIneedingMIO_SHAREDon Windows, which the.pcdoes not set; theMIO_CELL_TYPES(X)X-macro behindenum mio_cell_type(lines 178–208), which bindgen expands; the function-likeMIO_VERSION_AT_LEAST, which it skips (re-express as aconst fn); enums asnewtypeconstants, since the C side may add values. Avendoredfeature that builds the C library with thecmakecrate.meshioplusplus: safe wrappers.Meshowns a*mut mio_meshand frees it inDrop; the borrow window is the lifetime system's job: getters return&[T]orndarray::ArrayViewtied to&self, and every mutating method takes&mut self, so "borrows expire on mutation" is checked at compile time. Option structs are built by*_opts_initthen field assignment behind builder types, never by literal (the reserved tails must stay zero). Errors: aResult<T, Error>carrying the status andmio_last_error().
- Steps. (1)
bindings/rust/{meshioplusplus-sys,meshioplusplus}as a workspace, version in step with the others (a row inAGENTS.md's version table). (2) Tests ported from the Julia suite's sections (bindings/julia/MeshioPlusPlus/test/runtests.jl: introspection, shape identity, the borrow window, file round trip, regions, each operation, errors carryingmio_last_error(), the XDMF series). (3) A parity guard, which no binding but Octave has today:tests/python/test_binding_parity.pyparses the C header's function names and checks each is wrapped or listed as exempt, for Julia (ccall(_sym(:mio_…))insrc/*.jl), R and Rust alike. (4) ArustCI job in the pattern ofjulia(ci.ymllines 2488–2584: build the C API, run the tests against it, run the doc example, check that a missing library fails by name). (5)doc/rust.mdon the template ofdoc/julia.md(building, array layout, the borrow window, memory, errors, documented gaps, tests), in the Language surfaces sidebar group (doc/.vitepress/config.mtsline 253), and the binding lists inAGENTS.md. (6) crates.io publication from the release workflow. - Done when.
cargo testpasses against an installed C library on the three platforms, and the parity test covers everymio_function.
4.3 Interop phase 2: Open3D and DOLFINx
- State today.
src/python/meshioplusplus/_interop.py:to_open3d(lines 1662–1668) andto_dolfinx(1671–1678) call_phase_two, which raisesNotImplementedError; neither is in__all__;has_open3dandhas_dolfinx(1025–1032) return a hard-codedFalse. The working bridges share a pure payload layer (_to_vtk_payloadline 469,_to_triangles_payload698,_to_table_payload882; dataclassesVtkPayload,TrianglesPayload),_require(module, extra, op)(1035) for the install hint, and_copy_note/_emitfor the copy report. Interop records the intended_to_tetra_payloadand_to_dolfinx_payloadwithcell_perm_gmsh. - Steps.
- 4.3.1 Open3D.
to_open3d(mesh, kind="auto")from_to_triangles_payloadtoo3d.geometry.TriangleMesh(orPointCloudfor a vertex-only mesh,TetraMeshthrough a new_to_tetra_payload), using the tensor API (o3d.t.geometry, which takes numpy without theVector3dVectorcopy the stub's message cites) where present;from_open3d; colours and normals as point data.has_open3dbecomes an import probe (_importable). Anopen3dextra. S - 4.3.2 DOLFINx.
to_dolfinx(mesh, comm=MPI.COMM_SELF): one cell type only (refuse a mixed mesh by name), the coordinate element frombasix.ufl.element("Lagrange", cell, degree, shape=(gdim,)), connectivity permuted from VTK to DOLFINx order withdolfinx.io.utils.cell_perm_vtk,dolfinx.mesh.create_mesh; cell regions tomeshtags.from_dolfinxthroughmesh.geometry.xandmesh.geometry.dofmap. DOLFINx is conda- or apt-only, so no extra: the hint names both. Verify first against the current DOLFINx release's API, which has moved between minors. M - Both: export in
__all__with the lazy map (__init__.pylines 501–518),_NOT_TOOLSentries intests/python/test_mcp.py(lines 2491–2511 hold the interop ones), gated tests intests/python/test_interop.pyin the pattern of the trimesh block (line 646), Open3D in theinteropjob's install line (ci.ymllines 1813–1847) and a DOLFINx leg in the conda-basedparaview-pluginjob's pattern, the Phase 2 section of the page, a notebook cell.
- 4.3.1 Open3D.
- Done when.
test_phase_two_targets_are_not_implemented(line 481) is deleted because both are.
4.4 A CLI chatbot
meshioplusplus chat, a natural-language entry point from the terminal. The MCP server already exposes the whole Python surface to an agent, but only to a host that speaks MCP; a user with an API key cannot ask the CLI "why does this file fail to convert" or "clean this mesh and tell me what changed". A thin client over existing work, not new mesh functionality. S–M
- State today.
TOOL_REGISTRY(src/python/meshioplusplus/mcp/_tools.pyline 7665) maps 203 names to{"fn", "wraps", "gated"}with no schemas;call_tool(name, kwargs)(8517) dispatches andguard(8507) turns exceptions into{"error", "error_type"}; the sandbox is_resolve(325–349) under_ROOT(set_root, 269); results pass_sanitize/_json_safe(352–391).mcp/_http.pyalready dispatches/api/tools/{name}throughcall_tool(lines 200–220). Schemas exist only as the typed wrappers ofmcp/_server.py, which need themcpSDK to list. - 4.4.1 Reuse, not reimplementation. The loop calls
_tools.call_toolin-process: never a second dispatch, sandbox or sanitizer. Every tool added to the registry reaches the chatbot, under the paritytest_every_operation_has_a_toolgives. - 4.4.2 Tool schemas without the MCP SDK. The provider needs a JSON schema per tool, and the
chatextra must not requiremcp. Generate them from_server.py's wrapper signatures by introspection (inspect.signatureand the annotations, the same data the SDK reads) in a smallmcp/_schemas.py, with a test that it agrees withserver.list_tools()when the SDK is installed (test_server_lists_every_registered_tool,tests/python/test_mcp.pyline 3405, shows the listing). This is also the committed schema source 4.9.1 asks for. - 4.4.3 v1 scope. A stateless-per-turn loop (history and tool schemas out, tool calls executed, results fed back until a plain-text reply) over files under the working directory or
--root(set_root); context lives in the terminal session only; a tool allow-list flag and a--read-onlymode that withholds every tool whosewrapswrites; each tool call echoed before it runs. - 4.4.4 Packaging. A
chatextra carrying the provider's SDK, kept out of[all]; a new_cli/_chat.pywithchat_cmdand one_Verb("chat", "_chat", "chat_cmd", …)row in_cli/_main.py's_VERBS;test_one_verb_imports_its_module_alone(tests/python/test_cli.pyline 300) guards that nothing else imports it. Without the extra or a key the verb fails by name, as_require_serverdoes inmcp/__init__.py. Tests drive the loop with a scripted fake provider. Provider-agnostic wiring and persisted history are follow-ups. - 4.4.5 Docs.
doc/chat.mdon the section layout of MCP (installation with the named error, running, the sandbox, what it can call), the sidebar,README.md, and the verb table of CLI. - Done when.
meshioplusplus chat --root examples "what cell types does bracket.vtu hold?"answers by callinginfo, with a fake provider in CI and a real one by hand.
4.6 The dataset manager shows the stored thumbnail
- State today. A manifest entry's
Thumbnailis a PNG path the Python API writes and reads (src/python/meshioplusplus/_dataset.py: key at line 49,thumbnail_path278,set_thumbnail715,render_thumbnail721). The browser manager (src/viewer/src/dataset/) parses and saves the key (manifest.tslines 47 and 178–190) but shows no stored picture: entries are list chips (main.tsrenderEntryList, lines 312–344), and the only image is the per-manifest overview card (overview.tslines 229–233), fed by a session capture (thumbs.tscaptureThumbnail, called frommain.tslines 598–607). A sandboxed file route already exists (GET /api/files?path=&token=,mcp/_http.pylines 222–239;ApiClient.fileUrl,api.tslines 126–132), and a picked directory can serve bytes (WorkspaceFile.bytes(),fs.tslines 25–31). - Steps. (1) A
thumbnailUrl(entry)resolver: the companion'sfileUrlwhen connected, else a Blob URL from the workspace file, else none. (2) Show it in the detail pane (renderDetail,main.ts352–378) and as a small image on each chip, lazily (loading="lazy", revoke Blob URLs on re-render). (3) The overview card prefers the first entry's stored thumbnail over a session capture. (4) "Save preview as thumbnail": write the capture as PNG into the workspace and set the key (the File System Access path isreadwritealready). (5) Tests:tests/viewer/dataset.spec.mjs(a manifest with a thumbnail shows an<img>with a non-emptynaturalWidth) andtests/viewer/unit/manifest.test.mjs; update datasets, whose line 101 says the manager does not display it yet; rebuild the embedded page (7.3). - Done when. Opening a dataset written by
render_thumbnailshows its pictures without opening any case. S
4.7 Split Python packages: Windows
v18.0.0 landed the provider registry, the shared core in the MIT base wheel, the meshioplusplus-{occt,netgen,mmg} add-ons, the permissive libraries linked static into the Linux and macOS base wheels, and plugin loading for the native CLI and the C API (plan). What is left is Windows. M
- 4.7.1 The permissive libraries on Windows.
build/build-permissive-libs.shrefuses MSYS and Cygwin (lines 30–36) and builds with-fPIC -fvisibility=hiddenandmake, so the Windows wheel setsWITH_HDF5=OFF WITH_NETCDF=OFF(wheels.ymllines 166–167) and Python falls back to h5py and netCDF4 (the__has_hdf5__-style flags,bindings/python/_core.cpplines 2005–2057). Steps: abuild/build-permissive-libs.ps1(or a CMakeExternalProjectsuperbuild that serves all three platforms frombuild/deps-pins.sh's versions and hashes) building zstd, LZ4, bzip2, HDF5, netCDF, CGNS and KaHIP static with/MD(the wheel's runtime;build-occt.sh --msvc-static-runtimeandbuild-cutcells.share the MSVC precedents) andCMAKE_POLICY_DEFAULT_CMP0091=NEW; HDF5's symbols stay out of the core DLL's export table becausecmake/gen_core_def.pyexports themeshioplusplusnamespace only; cache the prefix in the Windows leg as the other two do (wheels.ymllines 34–41); turn the options on inCIBW_ENVIRONMENT_WINDOWS. Probe:python -c "import meshioplusplus._core as c; assert c.__has_hdf5__"in the Windows wheel test, andMESHIOPLUSPLUS_STRICT_CORE=1reading an XDMF file. - 4.7.2 Plugins on Windows. A DLL binds its imports at link time against a named import library, so a plugin cannot borrow its host's symbols as an ELF or Mach-O one does (
ENABLE_EXPORTS,CMakeLists.txtline 2254;cmake/plugins.cmakestops with a fatal error onWIN32, lines 13–15). The wheel already solves it for Python: a sharedmeshioplusplus_core_meshio.dllwith a generated export table (gen_core_def.py, wired atCMakeLists.txtlines 1572–1589) that the add-ons link. For the native CLI and the C API the same core DLL would be the host both link, which conflicts with the CLI's static runtime (/MT, lines 2279–2284;cli.ymlbuilds OpenCASCADE with--msvc-static-runtime): exceptions and allocations must not cross a runtime boundary. Decision to take: a second Windows CLI flavour built/MDagainst the core DLL, shipped with it and able to load plugins, beside the single-file/MTexecutable that cannot. Then lift theWIN32guard, export throughsrc/cpp/providers/plugin_export.hpp, and enablePlugins.*(tests/cpp/test_plugins.cpp; todayNotBuiltOnThisPlatform, line 123) on Windows.
4.8 Provider plugins beside the native CLI release
v18.0.0 lets the native CLI and the C API load providers from shared libraries (MESHIOPLUSPLUS_PLUGIN_PATH, providers and plugins), but no release ships one: MESHIOPLUSPLUS_BUILD_PLUGINS is off (CMakeLists.txt lines 1803–1807), no workflow sets it, and cli.yml packages the executable and its licence files only. Reopened from the MMG and Polyscope non-goals, which keep both out of the executable itself. M each
- 4.8.1 MMG and Netgen plugins.
- 4.8.1.1 The licence texts first. The plugin archive must carry the LGPL texts and relinking notes, and the add-on wheels it would copy them from do not have them:
packages/python/meshioplusplus-{mmg,netgen}declare the licence inpyproject.tomland ship nolicense-files, noCOPYING.LESSERand no relinking note (cmake/addon_common.cmakeline 33 installs the target only). Fix the wheels in the same change: add the LGPL-3.0 and LGPL-2.1 texts and aRELINKING.md(the library is linked dynamically or its objects are provided; where to get the exact source, frombuild/deps-pins.sh) to both add-ons'license-files, as OpenCASCADE's texts already ship. A compliance gap in what is published today: take it before anything else in §4. - 4.8.1.2 Measure the risk. The executable links
-static-libgcc -static-libstdc++(CMakeLists.txtline 2278) withENABLE_EXPORTS, a plugin needs the host's C++ runtime (providers.hpp), and no test loads a plugin into that configuration. In the manylinux container ofcli.yml: build the fake plugin (tests/cpp/plugin/fake_plugin.cpp) and load it into the static executable; throw an exception across the boundary both ways. If it fails, the plugin-capable CLI is a second, dynamically linked flavour, as on Windows (4.7.2). - 4.8.1.3 Build and ship.
-DMESHIOPLUSPLUS_BUILD_PLUGINS=ONwith Netgen and MMG prefixes in the Linux and macOS legs ofcli.yml, in the same container and toolchain;meshioplusplus_plugin_mmgandmeshioplusplus_plugin_netgen(cmake/plugins.cmakelines 21–43) intomeshioplusplus-cli-plugins-<version>-<os>.tar.gzwith 4.8.1.1's files; MMG built without ELAS, as the wheel. Document in CLI and MMG backend. - Probe. In the release smoke step:
MESHIOPLUSPLUS_PLUGIN_PATH=plugins ./meshioplusplus --version | grep -E "Netgen yes, MMG yes"(the linemain.cppprints at 10788–10805), andadapton a cube exits 0. Windows waits on 4.7.2.
- 4.8.1.1 The licence texts first. The plugin archive must carry the LGPL texts and relinking notes, and the add-on wheels it would copy them from do not have them:
- 4.8.2 Polyscope as a viewer plugin. The executable stays free of OpenGL, GLFW and X11.
- State today.
src/cpp/cli/polyscope_view.cpp(view_meshline 252,screenshot_mesh263) is linkedPRIVATEwhenMESHIOPLUSPLUS_WITH_POLYSCOPEis on (CMakeLists.txtlines 2264–2275). Without it:view --backend polyscopereports the build flag (view_not_built,view_payload.cpplines 638–644),view --backend autofalls back to the terminal viewer on a tty, andscreenshotfalls back to the software rasterizer and exits 0 (main.cpplines 8536–8551). - Steps. A
ViewerProviderinproviders.hppbeside the four kinds there (mName,mView(const Mesh&, const ViewOptions&),mScreenshot(const Mesh&, const ViewOptions&, const std::string& path); named registration likeadd_tet_mesher:add_viewer,viewer(name),viewer_names()); a pure ABI addition (an ABI review row, as v18.0.0's were).cmd_viewandcmd_screenshotask the registry before their fallbacks.polyscope_view.cppbuilt asmeshioplusplus_plugin_polyscope(MIT) throughmeshioplusplus_add_plugin, added to 4.8.1.3's archive;--versionlists viewers. - Probe. A C++ test registers a fake viewer and
viewreaches it (it does not compile today); thecli-polyscopejob (ci.ymllines 2391–2479) loads the plugin instead of linking it. After 4.8.1.
- State today.
4.9 An MCP server for programs that embed meshio++
The MCP server is a Python process. A program that links meshio++ as a C++ library, through the C API or the single header, or that loads the WebAssembly package, has the operations but no way to offer them to an agent, and the Python-free native CLI has no mcp verb. L, design pass first
- State today. Native pieces that exist:
detail/json_value.hpp(core-private:JsonValue,parse_json,write_json, non-finite numbers written asnull), which the single header and WASM can carry, unlike the pipeline's JSON front end andpipeline_report_json, which need the optional nlohmann;src/cpp/cli/json_out.hpp(JsonOut, a streaming writer with no truncation and no non-finite count); the registry;apply_pipeline_stepandrun_pipeline_steps(include/meshioplusplus/operations/pipeline.hpplines 191 and 198) overPipelineStep {mOp, mParams}, with the step table generated fromtools/pipeline_steps.json(111 steps with each key's kind and default). The Python side: sandbox, read cache (_CACHE,_cache_limit,_cache_key;_tools.pylines 450–477), sanitizer (_MAX_ARRAY = 1000), 203 tools of which about 100 are named after a pipeline step and about 104 are not (generators, inspection, multi-mesh operations, datasets, training, rendering). No native MCP code exists. - 4.9.1 Design pass. Decide and write down in a
doc/mcp_native_plan.md:- The native tool set:
read/info/convert/formats/sniffthrough the registry; one tool per pipeline step type, dispatched throughapply_pipeline_stepand described frompipeline_steps.json(never a second dispatch); the generators and the two-mesh operations (merge,interpolate,diff,hausdorff), which the pipeline's v2 steps partly cover. Python-only tools (training, datasets, Arrow and Parquet export, the viewers) are recorded exemptions. - One source of truth for schemas: 4.4.2's generated schema file, committed, with the Python registration and the native server both checked against it;
pipeline_steps.jsongains the per-key descriptions it lacks. - The JSON layer: promote
detail/json_value.hppto what the server uses for parsing and writing, with the truncation wrapper and the non-finite count added there; the pipeline's JSON front end moves onto it too, which removes the nlohmann requirement frompipeline_report_json(a prerequisite, and a gain on its own for the single header and WASM). - Whether the Python server delegates its sandbox and sanitizer to the core once they exist there, or keeps a twin held equal by tests. Recommended: keep the twin for one release, then delegate.
- The native tool set:
- 4.9.2 The core server. An installed header
meshioplusplus/mcp.hpp:McpServerwithHandleMessage(std::string_view json) -> std::string(JSON-RPC 2.0:initialize,tools/list,tools/call,ping), so the host owns its transport;ServeStdio()for the common case;McpOptions(sandbox root, read-cache budget, tool allow-list, host name and version);AddTool(name, schema, callback)for host tools. The sandbox, cache and reports keep the Python server's contract (path sandbox, read cache, strict JSON): realpath confinement, LRU eviction within a byte budget keyed on the file's identity,NaNasnullwith a count, arrays longer than 1,000 elements truncated. HTTP and the dashboard stay Python's. - 4.9.3 Surfaces. C: an opaque
mio_mcp_serverwithmio_mcp_server_create(const mio_mcp_opts*),_handle(server, json, out, out_len)in thesnprintfconvention,_serve_stdio,_free, andmio_mcp_server_add_tooltaking a callback with a user pointer: the header's first callback typedef, so state its threading and re-entrancy rules there. The native CLI: amcprow inkCliVerbs(src/cpp/cli/main.cppfrom line 10042). The installable C++ API and the single header. WASM:mcpHandle(json)and an integer handle in the module table, the conventionjs_bindings.cpplines 10441–10462 explain. Thin Fortran, Julia, R and Octave wrappers over the C entry points. - 4.9.4 Tests and docs. A parity guard beside
test_every_operation_has_a_tool: every native tool is a Python tool of the same name and schema, every Python tool is native or a recorded exemption; a scripted JSON-RPC session against the native CLI in themcpjob (ci.ymllines 1972–2031, which already runs both SDK majors); a fuzz target onHandleMessage(3.12.5); an Embedding the server section in MCP and the C, WASM and C++ pages; an ABI review row for the new header. - Probe.
meshioplusplus mcpis an unknown verb in the native CLI, andmio_mcp_server_handledoes not exist.
4.10 Interop phase 3: scikit-fem and xarray
Bridges in the interop pattern of 4.3. No module imports either today; verify first each package's current data model. The gmsh Python API is not one of them: it is GPL and in-process, which the decision on Gmsh's code excludes (non-goals). S each
- 4.10.1 scikit-fem.
to_skfem(mesh): one cell type, toMeshTri/MeshQuad/MeshTet/MeshHexand their second-order variants,pas points transposed andtas connectivity transposed with skfem's local ordering (the hexahedron differs from VTK's: a permutation in the payload layer, tested againstskfem.io.meshio.from_meshio, which is the reference implementation of the same mapping for meshio); cell regions tosubdomains, side regions toboundariesthrough(cell, local facet)to skfem facet indices.from_skfem. Extraskfem. - 4.10.2 xarray.
to_xarray(mesh, location="point"): aDatasetwith apoint(orcell) dimension, acomponentdimension for vectors and tensors, the coordinates as a coordinate variable; for a sequence,sequence_to_xarray(path)adds atimedimension lazily throughdask.delayedover the sequence reader, which is what makes it more thanto_pandas. With 2.4.3.2, UGRID attributes on the dataset soxugridanduxarrayaccept it. Extraxarray. - Exports,
_NOT_TOOLS, gated tests, CI install line and page section as in 4.3.
4.11 A .NET binding
Over the C API, after Rust (4.2). Desktop pre- and post-processors and their plugin APIs are largely .NET (Rhino and Grasshopper, the Ansys ACT and SpaceClaim APIs, Femap's and Simcenter's automation). M
- Design.
bindings/dotnet/MeshioPlusPlus/:LibraryImportsource-generated P/Invoke declarations overmeshioplusplusfornet8.0; handles asSafeHandlesubclasses calling the_freefunctions; option structs as[StructLayout(LayoutKind.Sequential)]mirrors initialised by the*_opts_initcalls, with a generated layout test against the sizes the C header documents (the checkc_api.cpp'sstatic_asserts make on the C side); getters asReadOnlySpan<T>over the borrowed pointer, valid until the next mutation (document it; a debug build tracks a version counter to throw on stale spans); errors asMeshioExceptionfrom the status andmio_last_error(). Generate the declarations from the header with ClangSharp, committed and freshness-checked, since 639 functions are too many to keep by hand. - Steps. The project and tests (xUnit, ported from the Julia sections as in 4.2); the parity guard of 4.2 extended; a
dotnetCI job on the three platforms against the built C API; a NuGet package withruntimes/<rid>/native/libraries from the C API build ofcli.yml's toolchains;doc/dotnet.md, sidebar,AGENTS.md's lists and version table. - Not queued. Go, Java and Swift: no consumer has asked, and each would follow the same pattern. A native Node addon: the WebAssembly package runs in Node.
4.12 A Jupyter widget
- State today.
view()in a notebook embeds a self-contained page per cell:build_page(src/python/meshioplusplus/_viewer_browser.pyline 170) fills three payload slots of_viewer_assets/viewer.html(a base64 VTP, stats, the view configuration) andview_browser(255) shows it as adata:URL iframe (303–310). It talks to nothing: no picked cell comes back to Python, and a changed mesh re-sends the 1.5 MB page. The viewer's state iswindow.__viewerState(src/viewer/src/main.ts, withpickedandselection). - Steps. An anywidget
MeshWidget(src/python/meshioplusplus/_widget.py, awidgetextra): the front end is an ES module built fromsrc/viewerby a third Vite target (build:widget, besidebuild:webandbuild:embed) exportingrender({model, el}); traitlets forcolor_by,time_step,clip,camera(Python to JS) andpicked,selection(JS to Python, wired where__viewerStateis written); the mesh crosses as a binary buffer (the VTP bytes, not base64 in JSON), re-sent only when it changes.view(backend="widget")returns it; the iframe stays the default and the fallback. Tests: a Playwright test intests/viewer/loading the module with a fake model; a Python test of the traitlets without a front end. A notebook, viewer,_NOT_TOOLS. - Done when. Picking a cell in JupyterLab sets
widget.pickedin the kernel. S–M
4.13 A VS Code mesh preview
A custom read-only editor that opens a mesh file in the browser viewer through the WebAssembly package, with the inspection report beside it. It is what fuzzing already says exists (3.16.4). S–M
- Steps.
src/vscode/: apackage.jsoncontributingcustomEditorsfor the extensions the lean artifact reads (generated from the registry's extension table), anextension.tswhoseCustomReadonlyEditorProviderposts the file's bytes to a webview that loadssrc/viewer'sbuild:weboutput and the lean@meshioplusplus/wasmartifact (no native code, no Python;loadMeshioPlusPlus,src/wasm/src/index.mjsline 471), and showsreadMetadataandcheckConformityin a side panel. Bundled with the viewer's Vite build; a strict webview content-security policy (the readers parse untrusted files: the page gets no network and no file access beyond the posted bytes). Tests with@vscode/test-electronin avscodeCI job; packaging withvscein the release workflow; publication to the Marketplace and Open VSX. - Done when. Opening
bracket.vtuin VS Code shows it with its cell counts.
4.14 A C++20 named module
import meshioplusplus; beside the headers. M, design pass first
- State today. 283 installed headers under
src/cpp/include/meshioplusplus/with no umbrella header; the only single entry point is the amalgamation (src/single_include/meshioplusplus/meshioplusplus.hpp, 308,816 lines). CMake's floor is 3.15, and module scanning is switched off for the tidy audit (CMAKE_CXX_SCAN_FOR_MODULES=OFF). Targets are exported fromCMakeLists.txtlines 1610–1966 with componentsCandCXX. - Decisions to take first. (1) The module's shape: a module interface unit
meshioplusplus.cppmwith the headers in the global module fragment andexport usingdeclarations for the public names (no source change, and the ABI is the headers'), against re-declaring; recommended the first. (2) What is exported: everything outsidedetail::, generated from the headers by a script so it cannot drift, with a freshness check. (3) Whether the single header can be the global fragment, which would make a header-only module possible; measure its compile time first. - Steps. An umbrella header
meshioplusplus/meshioplusplus_all.hpp(generated; useful on its own);option(MESHIOPLUSPLUS_BUILD_MODULE)guarded byCMAKE_VERSION >= 3.28and a scanning-capable compiler, adding aFILE_SET CXX_MODULESto the installable core targets and exporting it; a second consumer projecttests/consumer_module/built incpp-installon GCC 14, Clang 17 and MSVC; C++ API. - ABI. Unchanged: the module wraps the same declarations. An ABI review row for the new umbrella header.
4.15 The training-set stores in the core
Reopened from the non-goal that keeps pmsh, zarr, cae and usd in Python. For usd and cae its reason holds. For zarr and pmsh it now costs something: a dataset meshio++ itself wrote cannot be opened from the browser viewer, the dataset manager, Julia, R or the native CLI, and the parity decision's "a Python-only path has no twin to check it against" applies to them as to any format. M, design pass first
- State today.
zarr/_zarr.pywrites the PhysicsNeMo layout with zarr-python 3 only (Zarr v3): root attributes (physicsnemo_mesh_type,__tensordict__, provenance), arrayspointsandcells, groupspoint_data,cell_data,global_data, chunks of at most 200,000 rows, Zstandard level 3 or none (lines 81–166).write_dataset's store (_ml.pynear lines 617–650) writes one group per entry with whichever zarr major is installed and its default codecs (Blosc under v2).pmsh/_pmsh.pywrites a tensordict memory-map tree:meta.jsonfiles and raw little-endian blobs. In the core: the JSON DOM (detail/json_value.hpp),vtk_codec_decompress_blockfor zlib, LZ4, Zstandard and LZMA (detail/vtu_binary.hpplines 107–157), but Zstandard and LZ4 are off by default (CMakeLists.txtlines 264–265; on in the wheels and the full WASM artifact) and there is no Blosc. - Decisions to take first. (1) Scope:
pmshfirst (JSON and raw blobs: no codec, days of work) and Zarr v3 with the two codecs thezarrwriter itself uses; Zarr v2 and Blosc stores are refused by name, withwrite_datasetchanged to write v3 with Zstandard so that everything meshio++ writes is readable. (2) Local directories only: no object-store client in the core; the browser reads a picked directory through the existing workspace layer. (3) A build without Zstandard reads uncompressed stores and refuses compressed ones naming the option, the TecIO pattern. - Steps. By checklist F:
formats/pmsh.{hpp,cpp}andformats/zarr.{hpp,cpp}(directory formats: the registry's directory sniffing, as OpenFOAM and Elmer); the Python packages become shims with the current code as the twin (plain_shims); writers too, by the parity decision; conformance declarations; remove the Python-only notes on the binding pages. - Probe.
meshioplusplus info store.zarrin the native CLI, and the dataset manager opening a.pmshentry.
5. Performance
Admission: a slowdown in a path a user hits, measured or verified in the code, with the shape of the fix named. Every item lands through landing a performance change: output byte-identical across backends, thread counts and the baseline digests, SEQ not slower, the ABI tier stated. The section closed in October 2026 with its decisions recorded in the non-goals; the November 2026 audit reopened it with what those decisions do not cover, and the review later that month reopened two decisions with a narrower shape (5.14, 5.15). The impact estimates come from reading the code and from the tier rows of benchmarks and the XL spike, not from new measurements: each item's first step is the measurement its probe names.
The tools every item uses. Rows are defined in src/cpp/benchmark/bench_ops.cpp through row(name, lambda) (line 521), read_row(op, format, ext, mesh) (774), file_row (801) and write_row (921); tiers are S 12, M 30, L 50, XL 120 cells a side (line 378). tools/bench_ops.sh [OUT.csv] [BACKENDS] [THREADS] -- --hash --tier S sweeps backends and threads and compares digests against BASELINE=<csv>. python3 tools/bench_instructions.py --check benchmark/instructions_S.csv is the pull-request gate (1% above the baseline fails; --record re-records it, on CI's ubuntu-24.04 toolchain, saying why). benchmark/bench.py --sizes S,M,L --formats a,b times formats from Python.
5.1 The harness first
The items below are only provable where the harness sees them. S–M
- 5.1.2 The missing rows.
- Result-file readers.
lsdyna_d3plot,nastran_op2,ansys_rstandxplthave no row inbench_ops.cpporbenchmark/instructions_S.csv, though each has a registry writer, soread_row("read_d3plot", "lsdyna_d3plot", …)works as it does for the 18 readers that have rows; give each a multi-step file (the readers' cost is in the state records).benchmark/bench.py'sall_formats()(lines 100–135) probably sweeps them already from Python: confirm, and record their first numbers in benchmarks. - Integrations with no benchmark at all. The WASM ingestion of a JS mesh (a
tests/wasm/bench.mjstimingwriteMeshandreadMeshon a 1M-tetrahedron mesh under Node: 5.9's probe); the physics-ML Python layers (benchmark/bench_ml.pyoversample_grid,select_points,neighbor_pairsand one dataset write); the ParaView and Blender converters (_paraview.py's payload functions andsrc/blender/addon/formats.py, timed without the host application). - An item below whose operation has no row adds one first, as the landing rules require: 5.14 needs
transform,mirrorand a pipeline row containing a transform.
- Result-file readers.
- 5.1.4
decimate_volume's ballooning star.- State today. The row runs at tier S only (
bench_ops.cpplines 1066–1080). Insrc/cpp/src/operations/decimate_volume.cppthe greedy loop (while (true)at line 401) pops a min-heap of(regime, score, lo, hi, versions)(lines 334–336, scored bydv_score, 338) and collapses onto one survivor whose star then grows; guard 6, the inversion test, walks every tet of both endpoints' stars (lines 526–540). After the ring cache of lines 389–398, benchmarks records 0.96 s and 10.3 s for sides 12 and 14 with a 696-tet star and 234,410 rejected collapses; the 110 s for 24,576 tets quoted earlier predates that cache and tier M is not re-measured. - Steps. (1) Re-measure sides 16 and 30 and record them. (2) Bounding the star changes which collapses happen, so decide the operation's output first: a valence cap (
mMaxValence, a collapse refused when the survivor's tet count would exceed it) against a score penalty proportional to the merged star's size; implement both behind a private switch and compare on the tier S and M cubes and one graded Netgen mesh: cells removed for the target, minimum dihedral angle and scaled Jacobian of the result (compute_quality), and time. (3) Ship the better one as the default if quality is no worse than today's, else as an option; add the tier M row; re-record the instruction baseline, saying why. A new option field is a Tier A layout change toDecimateVolumeOptions. - Done when. The row runs at tier M in seconds and decimate_volume states the rule. M
- State today. The row runs at tier S only (
5.2 The wheels' parallel runtimes
The Linux wheels build the THREADS backend since v19.0.0 (wheels.yml lines 143–147; installation), a std::thread pool in the core that needs no runtime library. S
- 5.2.1 The measurement. One manylinux wheel from a
wheels.ymldispatch against the last SEQ wheel (v18):python benchmark/bench.py --sizes L --formats vtu,xdmfandtools/bench_ops.shat 1, 4 and 8 threads on the same machine; numbers into the table of benchmarks, whose last sentence names this probe. - 5.2.2 macOS. The base wheel installs Homebrew's libomp (
CIBW_BEFORE_ALL_MACOS, line 112), sets no backend (soAUTOpicks OpenMP) and no repair command, leaving cibuildwheel's defaultdelocate. Adddelocate-listdeps --allon the built wheel to the job's test step and assertlibomp.dylibis bundled; if it is not, switch macOS to THREADS too. Then replace "to be confirmed in a wheel listing" in installation (line 53) with the result.
5.5 Operation hot spots
- 5.5.2
optimize_volume's worst-cell smoothing.- State today. The profile at tier S puts 73% of the row in
optvol_smooth_worst(src/cpp/src/operations/optimize_volume.cpplines 967–1114). Per vertex and iteration (8):angles_at(x)computes all six dihedrals of every star tet (line 1015); the central differences are already limited to the tets in the active band (lines 1036–1056); the Frank–Wolfe step takes at most 64 iterations; then a backtracking line search of 14 halvings callsf_at(line 1001), which evaluates volume andoptvol_min_dihedral(lines 438–469: six edges, two cross products and a square root each, with theacosskipped where it cannot be the least) for every star tet at every probe. The sorts of the face and edge tables are 7.4%, the passes that use them 21%. - Steps. What is left is the line search and
angles_at. (1) Per probe, evaluate only the tets whose least angle can fall below the incumbent within the step: bound each tet's angle change by the step length over its inradius (a Lipschitz bound computed once per vertex), and skip tets whose margin exceeds it; the argument that skipped tets cannot become the least is the correctness condition and goes in the page. (2) Early exit off_atas soon as one tet is below the acceptance thresholdfx + 1e-9, which cannot change the accept/reject decision. (3) Reuseangles_at's per-tet edge vectors for the first probe. - Constraints. The determinism pins of optimize_volume: serial, fixed order, byte-identical across backends; (2) and (3) change no arithmetic, (1) must be shown to change no accepted step on the harness (identical digests).
tools/optvol_probe.py(needs Netgen) measures the result on Netgen's meshes. - Done when. The
optimize_volumerow's instructions drop by a third with digests unchanged. M
- State today. The profile at tier S puts 73% of the row in
5.8 HDF5
- 5.8.2 The library lock around library calls only.
- State today.
src/cpp/src/detail/library_lock.hpp:LibraryLock(RAII over a recursive mutex) andLibraryUnlock, which gives the lock up for a scope and retakes it; used by the parallel deflate (detail/hdf5_util.cpplines 231 and 321) and the VTKHDF reader's assembly (formats/vtkhdf.cpplines 1100, 1120, 1654). The writers hold the lock for their whole body:write_vtkhdf(line 1577), the series writer's methods (2048–2169),write_xdmf(formats/xdmf.cpplines 829–831) andxdmf_time_series.cpp(eight sites). Giving it up aroundvtkhdf_collect(line 1231) was tried and measured no change on four concurrent writes of a 125,000-hexahedron box (0.63 s either way), the deflate being the bulk (benchmarks). - Steps. Reopen only on a profile that shows preparation dominating: an uncompressed write of a mesh with many small arrays, or XDMF's light-data assembly. Then split each writer into prepare (no library call, no lock) and write (locked), which 3.10.3's annotations can enforce. The invariant, no two threads inside the libraries, is unchanged.
- Done when. Either a profile is recorded showing nothing to gain, and the item closes into the non-goals, or four concurrent uncompressed writes scale. S–M
- State today.
5.9 WASM: bulk copies of JS arrays
- State today.
bindings/wasm/js_bindings.cpphas 47emscripten::vecFromJSArraycalls, element by element through embind, each followed by a second copy into theNDArray:float64_ndarray_from_val(line 541),int64_ndarray_from_val(548),ndarray_from_js_array(589), the polyhedron CSR (701–705), all reached fromval_to_mesh(681: points 688, cells 754, data 770–813, regions 838). The other direction already copies in bulk throughtyped_memory_view(float64_array_from, line 241, and thearr.call<void>("set", …)sites near 336 and 2871). - Steps. One helper
fill_from_js(val, T* dst, n): when the value is a typed array of the destination's element type,val(typed_memory_view(n, dst)).call<void>("set", v)writes straight intoNDArray::Uninitstorage; when it is a typed array of another type or a plainArray, keep the element path (orconvertJSArrayToNumberVector, verify against emsdk 6.0.3'sval.h).int64from aBigInt64Arrayis the case to test: the view type must match. Replace the three helpers' bodies; the 47 sites then go through them. - Probe.
tests/wasm/bench.mjs(5.1.2) writes a 1M-tetrahedron JS mesh withwriteMeshbefore and after;tests/wasm/smoke.mjsunchanged;tests/python/test_wasm_parity.pyunaffected. S
5.11 Sequences
The sequence driver keeps at most one mesh alive, by design (the streaming guarantee). M
- State today.
run_sequence_pipeline(src/cpp/src/operations/sequence.cppline 1170) reads each step throughsequence_read_step(528: sets the step and callsregistry_read), in the loop at 1232–1240; the twin is_sequence.py(run_sequence_pipeline, line 1287). Document keys are parsed inpipeline.cpplines 4259–4331 (Version,Input,Operations,Output,Mode,Parallel,Workers,Resample) and_SEQUENCE_DOC_KEYS(_sequence.py258). The in-file-timeline readers re-read geometry at every step (read_xdmf,read_exodus,read_vtkhdf,read_lsdyna_d3plot); no series reader class exists. The PhysicsNeMo pairing readsseries[step]andseries[step + offset](physicsnemo/__init__.pylines 454–455), so consecutive samples read each step twice. - Steps.
- 5.11.1 Read-ahead. A
Prefetch: 1document key (default 0; add it to both key lists andtools/pipeline_steps.json's document section): step k+1 is read on a second thread while step k computes, underLibraryLockfor the HDF5 and netCDF formats, with the cost of two meshes alive stated beside the streaming guarantee. Output order and bytes unchanged. - 5.11.2 Fields-only reads. A
ReadOptionsflagmFieldsOnlywith aMesh*to reuse (points and cells kept, data replaced), honoured by the four readers above when the file's geometry is static (XDMF's shared grid, Exodus always, VTKHDF'sNumberOfPointsconstant, d3plot without adaptivity) and ignored otherwise; the driver sets it from the second step on.ReadOptionsis pinned at 72 bytes intest_abi_layout.cpp: use its reserved space if any, else this is a Tier A change. - 5.11.3 One read per step in the pairing. A two-entry cache in
_read_samplekeyed by(series, step); the same in_temporal.py(line 171).
- 5.11.1 Read-ahead. A
- Done when. A 100-step XDMF series converts with one geometry read (a counter in the test), and the pairing's read count equals the number of steps plus one.
5.12 Profile-guided optimisation of what is released
Link-time optimisation was measured and left opt-in because counts moved both ways (benchmarks); a profile was never tried, and it is what tells the compiler which of those ways matters. M, measure first
- State today. No
-fprofile-*flag anywhere. The release CLI is SEQ and static (cli.ymlCMAKE_COMMON_ARGS, lines 56–67); the Linux wheels are THREADS.MESHIOPLUSPLUS_ENABLE_LTOis wired atCMakeLists.txtlines 155–165;-ffp-contract=offat 767–769 keeps floating point the same. - Steps. (1)
option(MESHIOPLUSPLUS_PGO "" OFF)with valuesgenerateanduseand a profile directory, appliedPRIVATEto the core objects,_coreand the CLI (GNU:-fprofile-generate/-fprofile-use -fprofile-correction; Clang:-fprofile-instr-generate/-fprofile-instr-usewithllvm-profdata merge; MSVC:/GENPROFILE//USEPROFILE). (2) Training run:meshioplusplus_bench_ops --tier Mplusbenchmark/bench.py --sizes Mover the text and binary formats; the workload is the harness, so it is versioned. (3) Measure four builds (plain, LTO, PGO, both) withbench_instructions.pyand wall time on tiers S to L;bench_ops.sh --hashmust report identical digests on all four, which is the gate. (4) If the gain is consistent (no row more than 2% slower, median faster by 5% or more), add the two-stage build tocli.ymland towheels.yml'sCIBW_BEFORE_BUILD; if not, record the table and close into the non-goals beside LTO. - Done when. The table is in benchmarks and the release workflows either use it or the decision is recorded. The default build is untouched either way.
5.13 Browser memory: the ceiling, then a 64-bit build
Several decisions rest on the browser's heap: the viewer's applies are not cached because a second copy of the mesh shares it with MEMFS, and the ceiling is a mesh of a few tens of millions of cells. S, then M, verify first
- State today. The link options (
CMakeLists.txtlines 1849–1857) set-sALLOW_MEMORY_GROWTH=1and noMAXIMUM_MEMORY, so the heap stops at Emscripten's default ceiling (2 GB unless raised; verify against emsdk 6.0.3). Lengths cross embind asstd::size_t(arr["length"].as<std::size_t>(),js_bindings.cppline 653) and-sWASM_BIGINTis noted at line 300. The loader picks sequential or threaded bycrossOriginIsolated(src/wasm/src/index.mjslines 68–71) and the full artifact only on request (473–480). - Steps.
- 5.13.1 Raise the 32-bit ceiling.
-sMAXIMUM_MEMORY=4GBon the four artifacts; a Node test that allocates past 2 GB; the largest mesh the viewer opens before and after, recorded in WebAssembly. Days of work, and it may be all that is needed. - 5.13.2 Memory64. Verify first: emsdk 6.0.3's
-sMEMORY64=1status, embind mappingsize_ttoBigInt(everyas<std::size_t>()and every typed-array length the typed-array channel passes must then be converted at the boundary, andindex.d.tsmust not change), the dependencies ofbuild/build-wasm-deps.shrebuilt forwasm64, Safari's support, and the slowdown of 64-bit pointers ontests/wasm/bench.mjs. If it holds: a fifth artifact (meshioplusplus_wasm_64.{mjs,wasm}) built bybuild/configure-wasm.sh --memory64, chosen by the loader only on request ({ memory64: true }), as full is. - 5.13.3 Measure the caching decision again on whichever of the two lifts the ceiling: the non-goal on caching between the viewer's applies cites the shared 32-bit heap.
- 5.13.1 Raise the 32-bit ceiling.
5.14 An in-place transform
Reopened from the decision against dropping transform's copy, which stands for transform itself: its result aliases nothing, and that is its contract. An explicit second entry point is an addition. S–M, measure first
- State today.
Mesh transform(const Mesh&, const AffineTransform&, bool rotate_vector_data)(include/meshioplusplus/operations/transform.hppline 87;src/cpp/src/operations/transform.cpplines 203–270) copies connectivity (transform_owned_copy, line 222), point, cell and field data. The pipeline'spipe_apply_transformalready takes its mesh by value (pipeline.cppline 491), then copies it again insidetransform. The uniform mesh API hasPoints()const andAssignPointsonly (mesh_api.hpplines 52 and 76): no mutable points accessor. No benchmark row holds a transform:pipeline_smooth_surfaceis the only pipeline row, andtransform,mirrorandflip_normalshave none. - Steps. (1) Rows first:
transform,mirror,flip_normals, andpipeline_transform_chain(three transforms then a write), at tiers S to L. (2) Measure what share of each the copies are (a build with the copies stubbed, for the number only). If under a few percent of the pipeline row, record it in the non-goal and close. (3) Otherwise:void transform_inplace(Mesh&, const AffineTransform&, bool)in the same header (a pure addition: ABI review row), implemented by taking the points array out of the mesh, mutating it when owned (MakeOwnedwhen it is a view) andAssignPointsback, rotating vector data in place likewise; the pipeline's Transform, Mirror and FlipNormals steps call it on their by-value mesh. Surfaces: Cmio_transform_inplace; Pythontransform(mesh, …, inplace=True)only when the mesh's arrays are owned and writable, a named refusal on a borrowed view; the flat bindings through the C entry. The data-only operations (data_drop,data_keep,data_rename,data_calc, data operations) are the same question with the points untouched: measure them in the same pass. - Done when.
pipeline_transform_chaindrops by the measured share with every other digest unchanged, or the number is in the non-goal.
5.15 Vector kernels behind runtime dispatch
Reopened from the decision against SIMD intrinsics and -march flags, whose reason was portability across wheels, WASM and the release binaries, with "revisit only if the harness shows a kernel dominating". The per-row profiles now exist, and function multi-versioning compiles the variants into one portable binary. M, measure first
- State today. No intrinsics,
targetattributes, NEON or WASM SIMD anywhere, and no dispatch scaffolding. Candidates, all integer or byte kernels: base64 (vtub_b64_groups,b64encode,b64decode;src/cpp/src/detail/vtu_binary.cpplines 60, 89, 224); byte swapping (bswap_copy,bswap_inplace;include/meshioplusplus/detail/byteswap.hpplines 105 and 140); number text (parse_double,snprintf_c;detail/fast_number.hpplines 157 and 378); the BVH's box tests (tribvh_slab,detail/tri_bvh.cppline 88;sd_box_lb2,detail/surface_distance.cppline 483); the gathers (cell_centroids,BoundingBoxUtils::FromCoords). - Steps. (1) Measure:
perf recordon the tier L rowsread_vtu_ascii, a base64vturead and write, a big-endian legacy VTK read,distance_to_surface, and report each candidate's share in benchmarks. (2) A kernel qualifies only if its vector form is bit-identical to the scalar one: byte and integer kernels are; the floating-point box tests are comparisons, which are; reductions (the centroid sums) are not and stay scalar. (3) For the two largest shares: adetail/dispatch.hpp(MIO_TARGET_CLONES("avx2", "default")on GCC and Clang for x86-64; NEON is baseline on arm64 and needs no dispatch; a manualcpuidfunction pointer for MSVC;-msimd128for the full WASM artifact only), with the scalar body unchanged and the vector body written with the compiler's vector extensions or Highway (Apache-2.0) if the extensions do not reach. (4) A test that runs each kernel's variants against the scalar one on random input, and the determinism check across the dispatch levels (an environment overrideMESHIOPLUSPLUS_SIMD=off). - Done when. The measured rows gain what (1) predicted with identical digests, or the measurement shows no kernel above a few percent and the non-goal stands.
6. Code duplication
Admission: copies the duplication measure finds, listed by place so each can be taken alone, without a feature waiting on it. An item names the files and line ranges, what is repeated, and the shape a shared version would take; it closes when the copies call one implementation or when the copy is shown to be a table that only looks repeated. Items whose shared home is one of §3.4's kernels say so and are counted there, not here. Line numbers are those of master at db32c73 (November 2026) and drift as the files change: rerun the report before starting an item.
Measured with tools/dup_report.py --lang all --json (static analysis), which works like SonarCloud's detector: C++ 22,294 of 244,439 code lines duplicated in 730 files (9.1%), Python 12,468 of 136,064 in 469 files (9.2%). SonarCloud's first analysis (§7) counts less, 4,974 duplicated lines in 314 blocks across 110 files (0.9%), because its C++ detector needs longer runs; its ranking of files agrees with the one below at the top (cli/main.cpp, the Nastran pair, mcp/_server.py, c_api.cpp, _core.cpp), and it found the copies of 8.10 the local report did not scan until it took the R binding's C sources. The report lists regions shared by two files; the copies within one file, which dominate the four worst files, were found with the same tokenizer and window over each file alone. Probe for every item: the file's duplicated lines drop in the report, tools/bench_ops.sh --hash against a BASELINE reports identical digests, and the instruction-count gate passes. A change to an installed header follows the ABI policy.
After the shared-utilities pass (November 2026: the report's table filter, the private MathUtils, GeometryUtils, BoundingBoxUtils, FacetSlotUtils, DTypeUtils and NDArrayUtils classes of shared geometry and FEM kernels, and the Python helpers it closed items into) the report, which now skips literal tables and reads the R binding's C sources, counts C++ 21,699 of 251,295 code lines duplicated (8.6%; 19,725 of 243,917 on the earlier roots, 8.1%) and Python 8,962 of 135,752 (6.6%). The numbering below is that pass's: what was 8.2 to 8.11 is now 8.1 to 8.10.
After the binding pass (November 2026: the helpers listed in shared geometry and FEM kernels, from _forward in the MCP server to mio_r_report_list in the R binding) the report counts C++ 20,554 of 250,625 code lines duplicated (8.2%) and Python 7,214 of 133,787 (5.4%). It closed 8.1.2, 8.2.3, 8.3.1, 8.6.1, 8.8.1 and 8.8.2, the server side of 8.4.1 and the C++ side of 8.4.2; it closed 8.1.3 and 8.1.4 and narrowed 8.3.2 by the decisions below, and narrowed the rest to what it found. Two of its closures raise their file's count, because the detector normalises names and literals and so matches the rows of a table against each other: registry.cpp's one row per format (263 duplicated lines before, 424 after) and cli/main.cpp's verb table (2,270 before, 2,348 after). Each lists every format or verb once where there were four and two lists; they are tables that only look repeated, the closing condition above. The decisions it took are not open work: _core.cpp's feature flags are one #if each with nothing a table would remove; the Python render keywords and the pipeline's Snapshot options are not one list spelt twice (15 keys exist only in Python, and the colours and encodings differ: tuples against hex strings); and a verb's text and --json reports are not one-to-one (column widths, conditional lines, keys only in JSON and their order against write_mesh_cli), so a report helper is per verb, as cli_sweep_report is, not one generic field list.
After the third pass (November 2026, the second half of the binding pass: the C API's result moves, lattice extents and enum checks; the native CLI's option, warning and lattice helpers and one decimate verb body; the MCP tools' _run_op; one factory for 20 Python format shims; the Nastran writers' model building; forced inlining for two of 3.4.11's shares; one spec for the pipeline step tables; the R and Octave bindings' report and getter helpers) the report counts C++ 19,411 of 249,708 code lines duplicated (7.8%) and Python 6,500 of 132,440 (4.9%). Every item of the binding pass's list closed or became a decision except the three below. The decisions, which are not open work: _core.cpp's remaining option fills each fill one struct once, with string-to-enum conversions that carry their own messages, so a generic (keyword, member, default) table would need a converter per entry to keep them; the C API's remaining inline conversions (mio_select, mio_mesh_cad_surface2, mio_fit_cad3, mio_boundary_layer) convert their struct once; of the native CLI's verbs only check-conformity prints the same fields as text and as JSON, so the per-verb report helper has nothing to share, and the four MMG verbs share their options, parameters and summary already, the rest being each verb's own fill; the MCP tools that transform an argument before the call (a path resolved, a list built) keep their bodies, since _run_op passes only call-free overrides; the primitives' checks are one shared call each, which a combined prologue would only re-wrap; the pipeline's per-step option parsing stays hand-written in each engine, with tests/python/test_pipeline_spec.py holding both to the spec's kinds and defaults; the time-series, svg/tikz and near-miss shims differ in their signatures or core arguments; the Nastran card tables and unique-id rules (per card in HDF5, global in OP2) and the readers' per-block policies (insertion against sorted order, last- against first-value-wins) differ by design; the R builders left (data_info, data_integrate, diff) build nested lists in loops. The viewer page's repeats are vtk.js's own WebGPU shader templates inside the single-file bundle, which sonar.cpd.exclusions leaves out of the duplication measure. The numbering below is this pass's: 8.7.1 became 8.1, what was left of 8.8.1 8.2, and 8.9 8.3.
After the installed-header pass (November 2026, ABI 42: the last three items, with the OpenCASCADE build no earlier pass had) the report counts C++ 18,744 of 249,629 code lines duplicated (7.5%; 19,411 of 249,784, 7.8%, before it) and Python 6,500 of 132,442 (4.9%), and this section has no open item. It closed 8.1 (the provider's pole and knot builds, node welding, and its copies of the native fit_cad's triangulation, adaptive options, re-segmentation rung, refinement verdicts and STEP chain walks, now fit_cad_native.hpp's and step_brep.hpp's; the fit_cad goldens unchanged), 8.2 (one gather body in detail/subset_gather.hpp, forced inline into build_cell_subset and clean: split, crop, partition, clean and clean_weld within the gate; a plain inline function, or an out-of-line entry beside the body in subset.cpp, cost build_cell_subset its inlining of read_int, split +10%) and 8.3 at ABI 42 (the grid walks, the link sentinels, the series writers' mode, raw array and special members, the drawing writers' options, the B-rep face counts and the distance names; see shared geometry and FEM kernels and common utilities). The decisions it took, which are not open work: the XDMF and VTKHDF writers keep their own declarations of WritePointsCells, WriteData, Flush and the rest, whose contracts and documentation differ (buffered light data against per-step HDF5 writes), and the MESHIO and NATIVE backends keep their own CellView and region and property-set accessors: every shared form measured (a CellView template over member pointers or a traits class, a common base, a CRTP base for the region accessors) moved GCC's inlining in the operations that read cells past the instruction-count gate (linearize_ragged +3.7%, gradient_surface +1.0% to +1.04%), and their point and data accessors sit over different storage (unordered_map against NamedArrays); mPadding/mPaddingRelative are two fields of five option structs with different defaults (0 or 0.1), not one struct spelt five times; the *_from_name parsers of octree_mesh and sdf each name their own aliases and messages; the format headers' read_<fmt>_metadata and <fmt>_time_values are one documented declaration per format, a table that only looks repeated. What the report still finds across the files the pass touched is outside this section: the hand-written cross products of step_brep.cpp and fit_cad.cpp (fcn_facet_area_fraction, the patch areas) are 3.4.1's. A copy the report finds from here on is listed in this section as a new item, numbered from 6.1.
7. SonarCloud findings
Admission: what SonarCloud's rules report beyond duplication, grouped by rule so a rule can be closed in one pass. A finding is either fixed, or shown to be a false positive and the rule narrowed for that pattern (static analysis), so the next scan does not report it again; a finding confirmed as a defect that loses data or crashes opens a Correctness debts section at the top, as how this file works says. The live list is on sonarcloud.io.
The first analysis ran on the default Sonar way profile, with no coverage: 416,477 lines; 110 code smells (maintainability A); 226 bugs (reliability E, 121 of them blocker); 46 vulnerabilities (security E). The SonarCloud pass (November 2026, ABI 41) worked every item of the list it made, from the 376 issues the scan of 60825af held, and closed them in code where the behaviour stays the same:
- Misread patterns. NaN tests are
std::isnan/math.isnan;NDArray's default heap is out of line, where the analyzer had followed its shape loop taking no dimension and reported every write past an array's first element (68 blockers); guards and locals the analyzer can follow replace its divisions and shifts by zero; the preprocessor branches that were alike are oneconstexprtest each. The Python floating-point equalities (88) were each read and are exact sentinels the reference must match the core on, recorded once for the rule. - Smells. The MESHIO data names are sorted on each call rather than memoized from a
constaccessor, so aconst Meshmay be read from several threads (mesh data model); atomics take the default order but one idempotent mark; the recursive and the deliberately leaked mutexes say why; parsing with side effects in&&is split; string lengths are bounded orstd::string_view; CLI handlers that only report return nothing; NumPy and Python idioms; the nine backtracking regular expressions are rewritten without overlapping quantifiers; the viewer's controls are labelled and its splitter follows the WAI-ARIA separator pattern. - Bugs to verify. None was a defect that loses data. The triangle intersection's slot count, the curve evaluator's dimension, the tensor and stiffness preconditions, the null connectivity and edge-table paths are explicit; destructors guard their logging; the C API's octree entry points keep their defaults' address in a local.
- Vulnerabilities. Real, and fixed: the TUI music player wrote its loop to a predictable name under the temporary directory (now a
mkdtempdirectory),make_temp_dirand the decompressed copies were not private (nowmkdtemp, mode 0700), the subprocess log was world-readable (0600), the MCP server'sresolve_checkpointfollowed the best checkpoint a job file recorded without confining it, and log lines carried a file's control characters unescaped (log::escape). By design, and recorded once per rule: the library's top-level modules and the trainer open what their caller names;dynlib_openloads the configured plugin; seeded sampling, a SHA-1 content key, the companion's plain-HTTP loopback URL.
What the pass could not settle here, because only a scan shows it. Configuration: sonar-project.properties (sources, exclusions, twelve sonar.issue.ignore.multicriteria entries each with its reason) and .github/workflows/sonarcloud.yml.
- 7.1 Confirm the third pass on the next
masterscan. The scan of015e109(November 2026) left 25 bugs and 7 vulnerabilities, all closed by the second pass (9b1b757), whose full re-analysis reopened four older ones and kept four code smells. The third pass replacedxdmf.cpp's tailmemsetby a fill over the array's own size (cpp:S3519), putproviders.cpp's twocpp:S8462markers back on the reported lines, and recorded per file the number formatters ofdetail/fast_number.hpp(cppsecurity:S5145) and the embedded viewer's focusable splitter and scroll region (Web:S6845). Steps: queryhttps://sonarcloud.io/api/issues/search?componentKeys=loumalouomega_meshioplusplus&resolved=false&ps=500(andapi/measures/componentfor the ratings), list what remains by rule in this section, and for each either fix it or add itsmulticriteriaentry with the reason (false positives). Done when the query returns none that is not listed here. S - 7.2 Coverage on new code. The
coveragejob uploads thecoverage-reportsartifact (coverage.xml,cpp-coverage-sonar.xml;ci.ymllines 1706–1715), whichsonarcloud.ymlfinds throughworkflow_run(lines 67–110) and passes assonar.python.coverage.reportPathsandsonar.coverageReportPaths. The quality gate's Coverage on New Code condition (80%) is then measured rather than failed outright. If the gate stays red, the uncovered new lines it names are the work, or the condition is set to what the suites reach; 3.11.1 puts the same number on pull requests, where it can be acted on before merging. S - 7.3 The viewer's built page is stale.
src/viewer/src/features/shell.tsline 39 sets the splitter'saria-valuenowwhen it moves, but the committedsrc/python/meshioplusplus/_viewer_assets/viewer.htmlholds the attribute once, the static one: its minifiedsetWidthpredates the change. Rebuilding needs the WebAssembly package (npm run build:embedinsrc/viewer, then copydist-embed/index.htmlover the asset,src/viewer/README.mdlines 44–108). CI's only check is a size drift of at most 10% (ci.ymllines 2171–2190), which cannot see this: add to theviewer-webjob, which builds the package anyway, a step that runsbuild:embedand fails when the result differs from the committed asset (or commits it from a scheduled job), so the embedded page cannot fall behind again. 4.6 and 4.12 need the rebuild too. S
8. Data model
Admission: something files hold and users ask for that Mesh has no place for, so that every format carrying it drops it, splits it or hides it in a naming convention. Each item may change NDArray or Mesh, and then it reaches every mesh backend (MESHIO, NATIVE, KRATOS), every binding and the ABI: each starts with a design pass in the manner of the long-run spikes, which decided the last two questions of this kind (NURBS as a side channel; the isogeometric layer beside Mesh), and the outcome may again be "a convention, documented and tested" rather than a new type. Each item lists the questions its pass must answer and the option recommended from what the code shows today. The order is by how many shipped formats lose data for want of each.
What a new dtype touches, for 8.1 and 8.2: enum class DType and dtype_size/dtype_numpy_str (src/cpp/include/meshioplusplus/ndarray.hpp lines 225–270); detail::DTypeUtils (src/cpp/src/detail/dtype_table.hpp, kNumDTypes = 10) and dispatch_dtype (detail/value_io.hpp line 180); dtype_from_numpy (bindings/python/np_conversions.hpp line 103, which today throws on kinds b and c, sending the call to the Python twin); mio_dtype (meshioplusplus.h line 156, values 0 to 9); Fortran's four constants (meshioplusplus.f90 near line 2281); Julia's _DTYPES (libmio.jl line 93); R's mio_r_fill_real, which widens everything to double; Octave's dtype_of_class (util.c lines 421–433); WASM's js_typed_array_ctor and dtype_of_js_array (js_bindings.cpp lines 303 and 559); every writer's dtype switch. A new enumerator appended is ABI Tier C for C++ and additive for C, but every switch over DType in the 86 format sources must handle it, which is the real cost.
8.1 Complex values
- State today. No complex dtype. The readers that meet complex data already agree on one convention:
<name>_realand<name>_imagin UNV (formats/unv.cpplines 1572 and 1639), OP2 (which converts magnitude and phase to real and imaginary first,op2_complex,formats/nastran_op2.cppline 1905) and Nastran HDF5 (formats/nastran_h5.cppline 522). EnSight's complex variables are skipped (2.3.3.5); Ansys.rstharmonic sets are neither read nor written (2.2.12). - Decisions to take first. (1) Convention or dtype. Recommended: the convention, since three formats already share it: document
_real/_imagin mesh data model as the form, add helperscomplex_pairs(mesh)(names that form a pair),to_complex/from_complexin Python (numpy has the dtype, so a Python user getscomplex128arrays on request) and a C++detail/complex_pairs.hpp, and have every writer that can store complex data (UNV 2414, OP2, Nastran HDF5, EnSight, VTK through two arrays) recognise a pair and write it as one complex field. (2) Which operations must treat a pair together:transformwithrotate_vector_data(rotate both),interpolate(both),data_calc(exposeabsandphase). (3) A real dtype only if a consumer needs complex arrays through the C API; thenMIO_COMPLEX64/128as interleaved pairs, by the list above. - Steps (the convention). The page section; the helpers with tests; EnSight complex read and write under it; a conformance property
complexinconformance_spec.pyfor the formats that hold it;data_inforeporting pairs as one field. - Done when. One complex OP2 table and one complex UNV dataset read to arrays of the same names and shapes, round-trip through each other's writers, and
to_complexreturnscomplex128. S (convention); L (dtype)
8.2 Strings and booleans
- State today.
field_datais numeric arrays only in the core (unordered_map<string, NDArray>in MESHIO,NamedArraysin NATIVE,native_mesh.hppline 450); Python'sMesh.field_dataaccepts any dict (_mesh.pyline 200), and the conversion throws on a non-numeric entry unlesslenient_field_dataskips it (np_conversions.hpplines 487–575), so a string infield_datasilently selects the Python twin or is dropped. Strings a format holds live in side structs (ExodusInfo::mInfoRecords,MedInfo::mMedNom). Booleans widen touint8(Octave maps logical toMIO_UINT8) and come back as integers. - Decisions to take first. (1) Strings in
field_data: recommended, a documented encoding rather than a dtype: auint8array of UTF-8 bytes with a companion<name>:offsetsint64array for a list of strings (the ragged layoutNDArrayalready carries for polyhedra), written bynp_conversionsfrom a Pythonstror list ofstrand decoded back, so the core carries them opaquely and every format writer that has a string slot (Exodus info and QA records, CGNSDescriptor_t, MED descriptions, VTKvtkStringArray) can use it. (2) Booleans: recommended, ameshio:bool:<array>marker infield_datanaminguint8arrays that are masks, honoured by the numpy conversion (returnsbool) and by the formats with a boolean type; no dtype. - Steps. The encoding in
detail/string_field.hppwith tests;np_conversions.hppboth ways; the C API needs nothing new (arrays of bytes) but getsmio_mesh_get_field_stringas a convenience; the four formats above; drop the silent fallback (astrinfield_datano longer declines the core). - Done when.
Mesh(..., field_data={"title": "wing"})writes through the core (MESHIOPLUSPLUS_STRICT_CORE=1) to Exodus and reads back equal. M, design pass first
8.3 Integration-point data
Gauss-point results are the primary output of a structural solver, and no operation knows an array is per integration point.
- State today. Three per-format conventions, each a cell array of shape
(cells, points × width)with its layout infield_data: Abaqus.fil(abaqus:layout:<name>=[points, width], point-major, NaN-padded; names with@sp<k>section points;formats/abaqus_fil.cpplines 1060–1148); d3plot (lsdyna_d3plot:layout:<name>,formats/lsdyna_d3plot.cpplines 1426 and 1992); GiD (gid:gauss_points:<name>andgid:gauss_coords:<name>,formats/gid.hpplines 275 and 303, the only one that records where the points are). MED's ELGA fields are read by the Python twin as a 3-D(cells, NGA, components)array, not written, and declined by the core (med/_med.pylines 695–719 and 1104)..rstelement results and Gmsh's$ElementNodeData(2.3.6.1) have the same shape.interpolate,data_averageandtessellatetreat the columns as components. - Decisions to take first. (1) Recommended: one convention replacing the three, not a third data location:
field_data["meshio:ip:<name>"] = [points, width, rule_id]on a 2-D cell array, withrule_idnaming a rule of 3.4.3's quadrature table (point positions and weights in the reference element), or 0 for "nodes of the cell" (element-nodal data) and -1 for "unknown rule" (the order is the file's); the readers write both their old key and the new one for one release. A third location (ip_data) would cost every backend, binding and writer for no operation the convention cannot serve. (2) Each format's rule and point order mapped to the table: Abaqus and Ansys element libraries, LS-DYNA's through-thickness points, MED'sGAUSSlocalisations (read from the file: MED stores coordinates and weights), GiD's internal and given rules. Where a format's order is undocumented, -1. (3) The operations:ip_to_points(extrapolate with the element's shape functions, then average at nodes: the least-squares form for rules with fewer points than nodes),ip_to_cells(weighted average), and the reversepoints_to_ipfor writing. - Steps.
detail/integration_points.hpp(layout read and write, rule lookup); the readers and writers of the five formats on it, with MED ELGA written and read in the core; the two operations by checklist O;data_inforeporting integration-point arrays as such; conversion MED to GiD and.filto MED as the round-trip tests. - Done when. A
.filstress field converts to MED ELGA and to a nodal field whose values match Abaqus's own extrapolation (.datprintout) on the truss and a C3D8 run. L, design pass first
8.4 Data on facets and edges
- State today. A
Regionholds a name, kind, dimension, tag and entries (include/meshioplusplus/region.hpplines 123–148;Sideentries are(global cell, local facet)rows, numbered bydetail/cell_faces.hppandcell_edges.hpp) and no values;Regionis 120 bytes intest_abi_layout.cpp. A value per boundary face today means a duplicated block of surface cells with cell data, which is what the VTK family and OpenFOAM's boundary fields use (the writer maps boundary-block rows toboundaryField). - Decisions to take first. (1) Recommended: no change to
Region; side data is cell data on the lower-dimensional block, and a side region can be turned into such a block and back:sides_to_cells(mesh, region)(the facets as a new block with ameshio:side_of(cell, local facet)cell array) and its inverse, so the formats that store values on sides (CGNSFaceCentersolutions and BC data, Exodus side-set variables, UGRIDlocation = "face"on volumes, MED face families) read into that form and write from it. This makes the convention explicit and testable instead of per format. (2) Edge data likewise onlineblocks withmeshio:edge_of. (3) A data member onRegiononly if a format needs values on sides with no cells to hang them on. - Steps. The two helpers by checklist O (they are
extract_skin's neighbours and reuse its facet table); the readers of 2.3.1.4, 2.3.2.3 and 2.4.3.2; a section in regions. - Done when. An Exodus side-set variable reads as cell data on a quad block that
sides_to_cellswould have produced, and writes back. M, design pass first
8.5 Hierarchy
- State today. Regions are flat; nesting is
/in the name (regions, lines 286–298), and the KRATOS backend is the one consumer that interprets it (BuildSubModelPartsFromRegions,kratos_mesh.hppline 610;RestoreRegions, 995), losing only names that contain.and themDim/mTagmembers. VTKHDF assemblies and.vtmflatten to it on read. - Decisions to take first. Whether
/names plus onefield_dataentry are enough to round-trip the trees files hold. Recommended: yes, withfield_data["meshio:tree"]as a string list (8.2) of the internal nodes that own no entity (an assembly that only groups), since leaves are regions already. Check against: Abaqus parts and instances (instances repeat a part with a transform: the tree names them, the transform goes tofield_data), VTKHDFAssembly,.vtmnesting, CGNS families of families, Exodus assemblies (2.3.2.4), USD prims. - Steps.
detail/region_tree.hpp(parse, build, parent and children queries) with the KRATOS interpreter moved onto it; the five formats' writers rebuild their nesting from it;regionsreports print the tree. - Done when. A nested
.vtmand a VTKHDF file with anAssemblyround-trip with the same nesting. M, design pass first; after 8.2
8.6 Structured grids that stay structured
- State today.
vti,vtr,vtsand legacy structured VTK are expanded to explicit cells bydetail::vtk_structured_cells(src/cpp/src/detail/vtk_xml_pieces.cppline 315): eight indices per cell for what three integers describe. Their writers accept only a mesh thatdetail::lattice_from_mesh(detail/grid_lattice.hppline 205) recognises as a lattice, which is why they fail conformance on the canonical mesh. CGNS structured zones are refused (2.3.1.3) and PLOT3D is not read (2.4.2). The physics-ML grids are the opposite case:GridSpec/GridArrayin Python with noMesh. - Decisions to take first. (1) Recommended first step: an annotation, not a block kind:
field_data["meshio:structured:<block>"] = [ni, nj, nk](point counts) set by every structured reader on the block it expands, checked bylattice_from_meshinstead of re-deriving, honoured by the structured writers (which then also accept curvilinear lattices forvts, CGNS and PLOT3D without geometric detection), and removed by any operation that changes the block's points or cells (the pipeline clears it unless the step is data-only or a rigid transform; an explicit list, tested). This closes the three conformance failures by declaring what the formats hold, and costs no ABI. (2) An implicit-connectivity block (CellViewgenerating corners from indices) only for 10⁸-cell voxel data: every operation's hot loop reads connectivity throughCellView, and the measured cost of changing it is what kept the backends'CellViews apart (§6). XL, and only with a workload. - Steps (annotation). The key and its invalidation rule in mesh data model; readers (
vti,vtr,vts, legacy VTK, VTKHDF image data, CGNS, PLOT3D, EnSightblock) set it; writers use it;conformance_spec.py's threeerrordeclarations become lattice round-trips;to_grid(mesh)/from_gridjoinMeshand the PythonGridArray. - Done when.
read("a.vts")thenwrite("b.vts")is exact, and the same mesh aftersmoothis refused by name. M (annotation); XL (block kind), design pass first
9. Operations
Admission: an operation a mesh workflow reaches for that the core's own kernels nearly give, found by reading each operation's page for what it refuses and by comparing the surface with what users of comparable libraries call. Not the algorithms the non-goals leave to backends. Each lands on every surface by checklist O; the anchors below are the kernels to build on, located in the code in November 2026.
9.1 Surface booleans through a backend
Reopened from the non-goals, which record why a native mesh_boolean was not queued: the spike was exact in topology and failed on rounding its output and on coplanar contact, the two things a dedicated library spends its complexity on. The same non-goal says the answer to such an algorithm is an optional backend, and that route was not measured. M, measure first
- State today. The prototype:
mesh_boolean_spike(A, B, BooleanOp)returningBooleanResult {mOk, mFailure, mMesh, mReport}withboolean:sourcecell data (src/cpp/src/detail/mesh_boolean.hppline 104). The harnesssrc/cpp/benchmark/boolean_spike.cpp(targetmeshioplusplus_boolean_spike) calls it directly at line 234 and prints one JSON line per run (ok,watertight,self_intersection_free,rounding_inverted, the volume identity errors, times).tools/boolean_corpus.pyhassynthetic,fetch,run,summary; the weeklyboolean_corpusworkflow runs it. Provider kinds are structs ofstd::functions ininclude/meshioplusplus/providers.hppwith named registration (add_tet_mesher,tet_mesher(name), lines 145–166); add-on wheels followpackages/python/meshioplusplus-mmgandcmake/addon_*.cmake; pluginscmake/plugins.cmake. - Steps.
- 9.1.1 A backend switch in the harness.
--backend spike|manifoldinboolean_spike.cpp, the Manifold path compiled only whenMESHIOPLUSPLUS_SPIKE_MANIFOLDfinds the library; no public API yet. - 9.1.2 Measure. Manifold (Apache-2.0†; verify its dependencies' licences, Clipper2's Boost licence among them†; guaranteed-manifold output from manifold input, coplanar faces by symbolic perturbation, its own WebAssembly build). Run the synthetic corpus (
--synthetic 78 200) and the Thingi10K pairs on the spike's own criteria: every result watertight (surface_watertight_check), free of self-intersections (find_intersectionson the output), the two volume identities, and the coplanar classes that the spike could not handle (boxes sharing a face, identical spheres, a vertex on a face). Also what it does with inputs that are not manifold (it refuses: count how many Thingi10K models that excludes, and whetherrepairfirst changes the count). Record the table in the spike's page beside its own. - 9.1.3 If it passes: the operation. A
BooleanProvider {mName, mVersion, mBoolean(const Mesh&, const Mesh&, BooleanOp) -> BooleanResult}kind withadd_boolean_provider,boolean_provider(name),boolean_provider_names();mesh_boolean(a, b, op, options)inoperations/mesh_boolean.hpp, refusing by name with no provider; point and cell data carried through Manifold's per-triangle provenance (boolean:source, and a parent cell id per output triangle socell_datamaps); ameshioplusplus-manifoldadd-on wheel and plugin; the full WASM artifact links Manifold's own wasm build if its size is acceptable (measure). By checklist O, with a pipeline step. - 9.1.4 What it unblocks. CSG of the primitives (
box - cylinder) and the scan cut by a fitted solid (the non-goal Booleans of a scan with fitted solids): remove both entries when it lands. OpenCASCADE's B-rep booleans stay the route for fitted solids that must remain analytic.
- 9.1.1 A backend switch in the harness.
- Done when. The corpus table is recorded; then either
meshioplusplus boolean a.stl b.stl --op unionworks with the add-on installed, or the measured reason joins the spike's and the non-goal stands.
9.2 Smooth subdivision
- State today.
refinesplits at midpoints (RefineOptions,operations/refine.hppline 356;refine_once,refine.cppline 292) andsubdivideis polyhedral; neither moves a point. No Loop or Catmull–Clark code exists anywhere (fit_cad's patch network fits B-spline patches by other constructions). Crease and boundary classification exists:crease_edges,crease_edges_trianglesandCreaseEdge {IsBoundary, IsNonManifold, IsCrease}(src/cpp/src/detail/crease_edges.hpp), already behindfeature_edges,decimateandsmooth. - Design. A new operation
subdivide_surface(mesh, SubdivideSurfaceOptions {mScheme: Loop|CatmullClark, mLevels, mFeatureAngleDeg, mPreserveBoundary, mRecordParentIds}), not an option ofrefine:refine's contract (points of the input are points of the output, bit for bit; green closure; hierarchy arrays) does not hold when points move. Loop for triangle blocks (Warren's weights; boundary and crease edges by the cubic B-spline curve rule; corners fixed), Catmull–Clark for quads and polygons (output all quads); creases fromcrease_edgesat the feature angle, semi-sharp not offered. Point data is subdivided with the same masks, cell data inherited by children, regions remapped throughdetail/region_remapwithFirstChild. Topology by one pass over an edge table (detail/edge_tableasrefineuses),parallel_forover points for the masks, serial numbering for determinism. A numpy twin with the same arithmetic order. - Tests. Limit positions on a regular patch against the closed form; a cube converging to its Catmull–Clark limit volume; a creased cube staying a cube at feature angle 30; determinism across backends (
thread_sweep.hpp). - Done when.
meshioplusplus subdivide-surface coarse.obj out.obj --scheme loop --levels 2on every surface. M
9.3 Ray and nearest-point queries
- State today. Two private-or-installed kernels and no public operation. Rays and winding numbers:
detail/tri_bvh.hpp(private):build_tri_bvh(const TriangleSoup&, LeafSize),tri_bvh_ray(tree, origin, dir, TMin, TMax, Skip) -> TriRayHit {mT, mTriangle, mNormal},tri_bvh_ray_filtered,tri_bvh_segment_hits,fast_winding/fast_windings; used bythickness,boundary_layer,tetrahedralize,octree_mesh. Nearest point: the installedinclude/meshioplusplus/detail/surface_distance.hpp:build_triangle_soup(mesh, region),build_distance_query,query_distances,query_closest_points -> ClosestPointHit,query_surface_projections -> SurfaceProjection {mPoint, mNormal, mDistance, mTriangle, mSourceCell, mFeature, mFound}; behinddistance_to_surface. Point location in volume cells is inline ininterpolate()(interpolate.cpplines 388–440: adetail::BucketTableover simplex boxes andinterp_tet_contains), with no reusable locator. - Design. Two batch functions first, a handle second. (1)
cast_rays(mesh, origins, directions, RayOptions {mFirstHitOnly, mMaxDistance, mRegion}) -> RayHits {mHit (uint8), mDistance, mPoint, mNormal, mCell (the source cell, through the soup's parent map)}overtri_bvh_ray,parallel_forover rays; andlocate_points(mesh, points, LocateOptions {mTolerance}) -> {mCell, mBarycentric or mReference}for surface and volume cells, extractinginterpolate's locator intodetail/point_locator.{hpp,cpp}(withinterpolateon it, digests unchanged) and 3.4.4's barycentric helpers. Both rebuild the tree per call. (2) Only if a caller needs many batches on one mesh: aSurfaceQueryhandle owning the soup and both trees, following the existing handle conventions: Cmio_surface_query_create/_freelikemio_sequence(meshioplusplus.hlines 6177–6229), Julia a mutable struct with a finalizer, R an external pointer, WASM an integer handle in the module table (js_bindings.cpplines 10441–10462), pybind apy::class_. - Surfaces. Checklist O; arrays in, arrays out, so the flat bindings need no new conventions for (1). MCP tools write the hits as a point-cloud file.
- Tests. Rays against an analytic sphere and a box; grazing rays (edge and vertex hits counted once);
locate_pointson every volume cell type with points on faces; agreement withinterpolateon a field (f(locate(p))equalsinterpolate's value bit for bit). - Done when.
cast_raysandlocate_pointson every surface; the handle only on request. M
9.4 Mass properties
- State today.
compute_stats(operations/stats.hppline 47:StatsReport) givesmTotalArea,mSignedVolume,mUnsignedVolumeand anmCentroidthat is the mean of the points, not the centre of mass;measureis GD&T, not mass.data_integrateintegrates a field per region withdetail::cell_measure(detail/data_ops.hppline 243).poly_measure(detail/polyhedron.hppline 198) returns a true centroid per polyhedral cell;closed_surface_volumeis private to the boolean spike. - Design.
mass_properties(mesh, MassOptions {mDensity (a scalar or a cell array name), mRegion, mAbout: Centroid|Origin})returning, per region and in total:mMeasure(volume, or area for a shell withmThickness),mMass,mCentre, the inertia tensor as six componentsxx yy zz xy yz zx(the symmetric-tensor order the data model uses), principal moments and axes (the Jacobi solver indetail/), radii of gyration. For a closed triangle surface: the divergence-theorem polynomials (Eberly's formulation; exact for a polyhedron), sharing 3.4.2'sClosedSurfaceVolume; for volume cells: tetrahedra exactly, other types by simplexification (convert_cells' tables) so curved cells are approximated as elsewhere and the page says so; an open surface is refused by name unless treated as a shell. - Also. State in stats that
centroidis the point mean, and addcentre_of_massthere from this kernel; changing whatcentroidmeans would be a silent change of a reported number, so it is not changed. - Tests. Box, cylinder, sphere and torus against closed forms; a translated and rotated body obeying the parallel-axis theorem; density per region.
- Done when.
meshioplusplus mass part.stl --density 7850 --jsonon every surface. S
9.5 Elevation beyond order 2
- State today.
ccells_elevate(src/cpp/src/operations/convert_cells.cppline 835) refuses the full-Lagrange targetsquad9,hexahedron27,wedge18andpyramid14(lines 845–849,ccells_is_full_lagrange, 191–194) and has no entry above order 2 (ccells_elevate_table, 168; twin_ELEVATE,_convert_cells.pyline 99). The pieces exist: arbitrary-order Lagrange bases andinterpolation_matrix(Shape, P, nodes, targets)(src/cpp/src/formats/lagrange_common.hppline 721; twin_lagrange.pyline 454),fixed_lagrangenaming the fixed types to order 10, and the node-ordering registry (detail/node_order.hpp).tessellatehandles five quadratic types only. - Design.
convert_cells(mesh, "elevate", order=q)and"reduce", order=qfor any supported shape: the new nodes are the geometry interpolated at the target lattice's nodes withinterpolation_matrix(order p to q), deduplicated across cells by their owning edge, face or interior (an edge table and a face table keyed by sorted corner ids, as the order-2 path does for mid-edge nodes); point data interpolated by the same matrix; fixed types up to the registry's limit, VTK Lagrange ragged cells above. Elevating a linear mesh gives straight-sided high-order cells; curving them isproject_to_cad's job afterwards. After 3.4.3's first step (the header moved out offormats/), which is a rename. - Tests. Elevate then reduce is the identity on points and data; a polynomial field of degree q is reproduced exactly at the new nodes; node counts against the closed form per shape; VTK reads a written order-4 mesh with positive Jacobians (the
pyvistagated test). - Done when.
convert_cells(elevate, order=3)on a tetrahedral mesh givestetra20on every surface, andorder=2, full=Truegiveshexahedron27. M
9.6 Repeated and mixed green refinement
- State today.
refinenames two limits. A later selective pass refines the green (transitional) cells directly, so repeated passes over one region degrade quality without bound. And mixed-type green (a quadrilateral closed into triangles, a hexahedron into pyramids) is not offered, because one input block would emit several output blocks, which theFirstChildcell-map contract cannot express (RefineResult::mCellMaps,operations/refine.hppline 427;CellMapKind {Direct, FirstChild, Global},include/meshioplusplus/detail/region_remap.hppline 100).undo_green(coarse, fine)exists (operations/undo_green.hppline 134) but needs the coarse mesh and undoes a single-pass hierarchy only; the hierarchy reader isdetail/refine_hierarchy.hpp. - Steps.
- 9.6.1 Undo before refining. A
RefineOptionsflagmUndoGreenFirst(from the reserved space:mio_refine_optshasreserved[5]free; the C++ struct is pinned at 120 bytes, so a new member is Tier A): before a selective pass, cells whoserefine:entitymarks them green are merged back to their parent usingrefine:parent_idandrefine:cell_idalone (no coarse mesh: rebuild the parent's corners from the children's, which the templates ofdetail/refine_templates.cppmake possible), the parent is marked for red refinement if any child was selected, and closure runs again. Requiresrecord_hierarchy, and raises by name without it. Extendsundo_greento multi-level hierarchies as a by-product. M - 9.6.2 Mixed-type green. Decide the map shape first:
CellMapKind::Global(one global child-range per input cell) exists internally; expose it forrefinewhenmClosure = MixedGreen, with every binding's map accessor returning the global form. Then the templates: quad with one or three marked edges into triangles, hexahedron into pyramids and tetrahedra around a refined face. M–L, design pass first
- 9.6.1 Undo before refining. A
- Done when. Ten selective passes over the same region keep the minimum scaled Jacobian above a stated bound (a test that fails today), driven by
error_sizein a notebook.
9.7 Partitioning
- State today.
PartitionMethod {SFC, KaHIP, Auto}(operations/partition.hppline 85); KaHIP is a compile-time option (MESHIOPLUSPLUS_WITH_KAHIP,CMakeLists.txtline 319), not a provider, called inpartition_kahip_parts(partition.cpplines 197–267) on the facet dual graph ofdetail::build_facet_dual_graph(detail/dual_graph.hppline 66) with weights frommWeightsKey; without it the method throws by name (lines 274–284). METIS and Scotch are graph files only (write_graph,operations/adjacency.hppline 133). The C API passes the method as a string (mio_partition,meshioplusplus.hline 3082), so a new method needs no C change. - Steps.
- 9.7.1 A partitioner provider kind.
GraphPartitionerProvider {mName, mVersion, mPartition(const DualGraphCsr&, nparts, imbalance, seed, weights) -> std::vector<int64_t>}withadd_graph_partitioner,graph_partitioner(name),graph_partitioner_names();partition's method becomes a name looked up there aftersfc; KaHIP moves onto it as a compiled-in provider with its behaviour and digests unchanged.PartitionMethodgains no enumerator: addPartitionOptions::mMethodName(Tier A) or keep the enum for the built-ins and add the name beside it. - 9.7.2 METIS and Scotch providers. METIS (
METIS_PartGraphKway, Apache-2.0 since 5.1.1†) and Scotch (SCOTCH_graphPart, CeCILL-C†, LGPL-compatible) as optional compiled-in providers and plugins, in the pattern of the Netgen provider (src/cpp/providers/netgen/), each with a CI job likekahip(ci.ymlline 591). Seeds fixed, so output is reproducible per library version; state that it is not across versions. - 9.7.3 Connected parts. KaHIP's
--connected_blocksis not reachable through thekaffpaC interface (partition records it). A method-independent post-passmConnectedParts: label components per part withcell_components(detail/cell_components.hpplines 55–60), keep each part's largest, and reassign every other component to the neighbouring part it shares most facets with, repeating until none remains; report the balance after it.
- 9.7.1 A partitioner provider kind.
- Done when.
partition(mesh, 8, method="metis")works with the provider present and refuses by name without, andconnected_parts=Truegives one component per part on the space-filling curve's output. S–M
9.8 The isogeometric layer's open list
IGA marks four things as not yet. M
- State today.
iga_evaluate(info, patch, params, Derivatives)accepts 0 or 1 (operations/iga.hppline 154); second derivatives exist internally (iga_basis_ders2,iga_eval_surface_d2,detail/iga_kernels.hpplines 514 and 574;BrepSurfaceEval::Eval2); closest-point projection exists for B-rep faces (SurfaceProjector::ProjectandPolish,detail/surface_project.hpp, used byproject_to_cad); knot insertion and degree elevation are public (iga_insert_knots,iga_elevate_degree); VTK Bézier cell types are incell_type.hpp(lines 126–132) withRationalWeights/HigherOrderDegreesread byvtu_read.cpp. - Steps.
- 9.8.1 Second derivatives.
Derivatives = 2iniga_evaluateover the internal kernels;IgaEvaluation::mDerivativesgains the second-order block (its layout documented); the twin repeats the arithmetic order. S - 9.8.2 Bézier extraction.
iga_bezier_extract(info, patch): knot insertion to full multiplicity per span (the existing line-wise kernel), returning either the extraction operators per element (what an IGA code assembles with) or a mesh of VTK Bézier cells withRationalWeights, which the VTU writer already writes. S–M - 9.8.3 Knot removal.
iga_remove_knots(patch, dir, knots, tolerance)(The NURBS Book A5.8), reporting the knots it could not remove within tolerance. S–M - 9.8.4 Closest point on a patch.
iga_closest_point(info, patch, points) -> {params, points, distances}generalisingSurfaceProjectorfrom B-rep faces toNurbsInfopatches (curves and surfaces; volumes by inversion, later). S
- 9.8.1 Second derivatives.
- Done when. The Not yet line of the page holds only refinement of one patch of an MFEM model.
9.9 Point-cloud conditioning
- State today.
estimate_normals(operations/point_normals.hppline 112) andreconstruct_surfaceexist; the neighbour kernels arePointKnn(src/cpp/src/detail/point_knn.hppline 51: exactNearestandKNeareston a bucket grid) andneighbor_pairs(operations/neighbors.hppline 84). Downsampling exists in Python only and by count:select_points/subsample_pointswith methodsfarthest,grid(voxel representatives then farthest-point) andrandom(src/python/meshioplusplus/_point_budget.pylines 95, 291, 361, 485; point budgets), over the core'sfarthest_point_order(detail/farthest_points.hppline 54), which is bound to Python alone. No outlier removal and no Poisson-disk sampling exist. - Design. One core operation
filter_points(mesh, PointFilterOptions)on vertex blocks (and on the points of any mesh withmPointsOnly), with steps applied in order: statistical outlier removal (mean distance tomKneighbours beyondmStdRatiostandard deviations:PointKnn), radius outlier removal (fewer thanmMinNeighborswithinmRadius), voxel-grid downsampling by size (mVoxelSize: centroid or nearest-to-centroid representative; the size-driven modesubsample_pointslacks), Poisson-disk sampling (mMinDistance, by dart throwing on the bucket grid in a fixed order for determinism). Returns the mesh with a point map and counts per step; point data gathered (detail/subset_gather.hpp).subsample_points'farthestandgridmove to the core through the same operation so the flat bindings get them, with the Python functions as shims and their outputs unchanged. - Licence note. The non-goal on AGPL scan tools applies: implement from the literature (Rusu et al. 2008 for the statistical filter; Bridson 2007 for Poisson disk), not from any scan tool's source.
- Done when.
meshioplusplus filter-points scan.ply out.ply --outliers statistical --voxel 0.5on every surface, andreconstruct_surfaceon its output in a notebook. S–M
9.10 Units and coordinate systems
9.10.1 Units of fields
- State today. Length only:
field_data["meshio:length_unit"]withmesh_length_unitandrescale_mesh_length_unit(include/meshioplusplus/detail/length_unit.hpp) and Python's_units.py(length_unit,set_length_unit,unit_name); used by CGNS (DimensionalUnits_ton the base,cgns.cpplines 647–690), DXF, UNV, MED, Radioss, STEP. MED's per-field units are kept in a side channel (MedInfo::mFieldUnits,formats/med.hppline 162;field_data["med:field_units"]in Python). A field's unit travels nowhere else. - Decisions to take first. (1) Representation: recommended, the seven SI exponents plus a scale:
field_data["meshio:unit:<array>"] = [scale, m, kg, s, K, A, mol, cd](8 doubles), so a conversion is arithmetic with no unit database in the core; names ("MPa") are parsed and printed in Python and the CLIs from a small table. (2) Which formats carry them: MED (per field), CGNS (DimensionalUnits_t/DimensionalExponents_tper array), Exodus (none: skip), EnSight (# unitslines in the case file), UNV (the 164 dataset's unit system: derive per data type), Selafin (variable-name units, 2.4.3.4), Nastran and Abaqus decks (none: a user-declared system). (3)convert_units(mesh, system): scales points, the length unit and every array with a known dimension to a target system (SI,mm-t-s,mm-kg-ms, …), refusing arrays with no declared unit by name unless told to leave them. - Done when. A MED file in millimetres and megapascals converts to SI and back to the same bytes. M, design pass first
9.10.2 Georeferencing
Only if 2.4.3 or 2.4.6 lands, since nothing read today is georeferenced. field_data["meshio:crs"] as a WKT2 or EPSG:<code> string (8.2's encoding), set by the UGRID, LAS and ADCIRC readers and written back; reprojection as a mode of convert_coordinates (CoordinateOptions, operations/coordinates.hpp line 119) through PROJ, an optional dependency in the TecIO pattern (2.4.6.2) with a Python twin through pyproj; without it the operation refuses by name. glTF and 3D Tiles get the ECEF convention only on request. S–M
Non-goals and decisions taken
Recorded so they are not re-proposed as gaps. A decision is reopened when what it rested on has changed, and then it is narrowed here, with the date and the number of the item that took it up; the November 2026 review read each against what has shipped since. It reopened five (surface booleans through a backend, 9.1; decomposed files read without MPI, 2.3.8; the zarr and pmsh stores in the core, 4.15; an in-place transform, 5.14; vector kernels behind runtime dispatch, 5.15) and four formats of Considered, not queued (2.4.5 to 2.4.8), and left the rest as they stand: the reasons recorded for the native constrained tetrahedralisation, NURBS as an entity kind in Mesh, Kokkos device execution, link-time optimisation by default (though a profile was never tried: 5.12), solver-coupled learning, contact mechanics, the copyleft codes and the CAD modeller have not moved.
Every registered format reads and writes, in both engines — decided in October 2026, when the parity tier closed; it reversed the earlier non-goals that kept result files and the export-only formats read-only. A format reads and writes in the C++ core and in its Python twin, so it reaches every surface (C, Fortran, Julia, R, WASM, both CLIs and the MCP server's format capabilities); a core-only path is unreadable from the surfaces that have no fallback, a Python-only one has no twin to check it against.
read(write(m))equalsmwithin the limits the format itself imposes (single precision, a cell type it has no slot for, a concept it cannot express): a loss that is a fact of the format is stated on its page and in the conformance notes, and one that is only an unwritten feature is a gap that reopens a Reader and writer parity section here. A new format or direction lands asAGENTS.mdsays (native writer registered inregistry.cpp, parameters throughWriteOptions, the Python twin behindcore_declined, the arrays a reader returns written back under the same names, aSPECdeclaration and a Writing section). The exemptions are recorded intests/python/conformance_spec.py(READ_ONLY,ENGINE_EXEMPT,PARTIAL_ENGINE_EXEMPT) and checked bytest_conformance.py:- Pure 2-D drawings.
svgandtikzdraw a mesh rather than store it: their readers parse only the structure meshio++'s own writers emit, return the geometry as planar (z = 0) and warn once per read that depth, and whatever else the drawing could not hold, is lost; anything else in the file is aReadErrornaming the construct, never a guess. ansys_rst_cyclic, which is not a file but the full rotor a cyclic.rstexpands to; its write side isansys_rst's cyclic option.stepandigesoutside a build with OpenCASCADE: reading a CAD model means a CAD kernel, which themeshioplusplus-occtadd-on wheel (since v18.0.0) and the release binaries carry and a source build finds when it can (skin to CAD, STEP); on WASM the lean build is the exempt one, and the full build carries the kernel.- One engine only, by decision: GiD (gidpost and its Gauss points are implemented once, in the core), the OpenFOAM case writer and its zone, field and multi-region reading, CGNS's face-based polyhedra (the writer's winding repair is a discrete branch two implementations could resolve differently), the CAD readers, and the
stepwriter's merged and fitted faces (faces="planar"/"fitted", withsegment's primitive method that feeds them: loop walking, vertex snapping and the primitive growth are discrete branches too, so the twin writesfaces="facet"byte for byte and nothing else); the Python entry points raise naming the missing core rather than guess.
- Pure 2-D drawings.
What the parity tier left as facts of the format, each stated on the format's page and in the conformance notes, none an unwritten feature:
nastran_op2writes the MSC dialect only (32-bit little-endian words, SORT1 tables). 64-bit words, big-endian files, NX/Simcenter's interlaced table names and double-precision GRIDs are read and detected, not written: a post-processor reads the MSC dialect with no flags, and whether NX accepts it is a vendor check (2.2.14). The random PSD, CRM and ATO tables are written as SORT1 magnitudes, as the reader reads them (MSC writes them SORT2, and pyNastran reads only the RMS and NO ones in SORT1); no SORT2 pivot is written. Elements on scalar points (CELAS3/CELAS4,CDAMP3/CDAMP4,CMASS3/CMASS4) andCBUSH1DandCBENDhave no cell type —Meshhas no scalar-point entity to hang them on — and are skipped with a warning.nastran_h5writes a table only where an MSC sample fixes its layout:CBUSH,CGAP,CMASSandCONM1elements, andMAT5,MAT8,MAT9,PBARL,PBEAMLandPCOMPGvalues, have no table in the six MSC Nastran 2020 files at hand, so the elements are left out and the values go on the plain cards, each with one warning; the OP2 file holds them.lsdyna_d3plotleaves the rigid roads' full rigid-body velocities (NDIM = 9) out of the state, and writes an airbag variable under a title-case name, since the names of a real file are in no manual available.radioss_animwrites the nodal normals as zeros:anim_to_vtkignores them and their 16-bit encoding is undocumented.abaqus_filwrites 8-byte words only, the reader's width; a single-precision build's 4-byte layout has no description or sample.ansys_rsthas no complex (harmonic) results, read or written (8.1 gives them a form once a sample exists): no public sample or open reader covers them, and DPF alone reads them. The nodal coordinate systems are written asansys:node_anglesand agree with pymapdl-reader only for single-axis angles, since it applies the three turns in the opposite order.xpltwrites a per-surface-node variable asFMT_NODEin surface order, where the reader averages the values a node on two surfaces has into one point value, which both surfaces then write.marcandmarc_t19: edge and face sets stay(cell, number)field data and side regions are dropped, since mapping them to facets needs Volume A's numbering (2.2.11); the Herrmann, bubble and generalized-plane-strain types come back as the default type of their shape, and a deck holds no data arrays.szpltdoes not write polygon and polyhedral zones: TecIO 2018.3, the newest source available, refuses them in an SZL file, so the reader refuses them by name too and the face-based zones stay with the.pltand ASCII readers. Partitioned output is for tetrahedron and brick zones of a static file only (TecIO's own limit), and a series is written whole.vtx: DOLFINx and ParaView read one cell type per file, so a file of several cell types or parts is meshio++'s own extension of the layout.
Generators the primitives leave out — recorded when roadmap §3.1 closed (October 2026), with the parametric primitives: generators driven by a user callback (no callback crosses the flat ABI; numpy users build
Mesh(points, cells)directly, andisosurfaceon agridcovers implicit shapes); scanned or modelled assets (the bunny, the teapot, the bracket fixture — data rather than parameters, each under its own licence); and CSG combinations, under the volume-meshing non-goal below (surface booleans were reopened as a spike and kept out on its measurements, findings) — two-body interface and contact fixtures are primitives placed withtransformand joined withmerge.MPI in the library — none planned (C++ API);
partition's ghost layers produce the halo an MPI assembly in the owning application needs. ParMmg, MMG's MPI layer, is excluded with it. Narrowed in November 2026: reading the files a parallel run left, one per rank, into one mesh needs no MPI, as the OpenFOAM andpvtureaders show, and is queued for the formats that lack it as 2.3.8.Solver-coupled physics-ML — assembled solver residuals, adjoints and Sobolev training, co-simulation, active-learning labeling, adaptive remeshing driven by a surrogate, and MPI model-part gathering. Every one needs a live solver (an assembly routine, its tangent, its communicator) and meshio++ has no notion of a discrete system; they belong in the application that owns the solver, the division Symbolic and physics describes.
The Python-only layers stay Python —
pmsh,zarr,caeandusd, and the physics-ML surface (grids, point budgets, proximity graphs, datasets, training) are export targets and tooling for a training pipeline, registered in Python rather than the shared C++ registry. A core kernel they call (the neighbour search) does not change that;tessellatemoved to the core (tessellation) because the native CLI and the flat bindings needed it, and itsTessellationhelpers stay Python. Narrowed in November 2026 for the two stores a dataset is written to: readingzarrandpmshin the core, so the browser, the flat bindings and the native CLI open what Python wrote, is queued as 4.15 behind a design pass;usd,caeand the physics-ML surface stay as decided.Polyscope in the release CLI executables — excluded deliberately, so
view/screenshotthere report the build flag rather than opening a window (viewer): the executable stays free of OpenGL, GLFW and X11. Narrowed in November 2026, once v18.0.0's plugins made it possible: a Polyscope plugin loaded beside the executable is queued as 4.8.2.General volume meshing algorithms as native operations — constrained tetrahedralization of a domain, hex-dominant meshing and general hex meshing from a CAD-like boundary, boolean/CSG, boundary-layer inflation and quadrangulation are each a library in their own right. (Narrowed in November 2026: geodesic distance, once listed here, is not one, since the heat method needs only the P1 operators and the sparse solver the core has, and shipped as
geodesic_distance; surface booleans were measured by a spike in November 2026 (exact in topology, but rounding its output to doubles left most curved results self-intersecting and coplanar contact unhandled, so no native operation is queued: findings, though the backend route this non-goal names was not measured for them and is, since the same month, 9.1); and v18.0.0's provider registry is the optional-backend route below, which external meshers and field-aligned quads now take as installed programs:tetrahedralize's TetGen and Gmsh andremesh_quads's QuadriFlow and Instant Meshes.) The answer is an optional backend (the KaHIP pattern), not a native implementation: volume domain meshing reaches meshio++ throughtetrahedralize(v17.23.0: natively by its octree backend, the skin fitted by isosurface stuffing, and exactly by Netgen, the skin kept) and adaptation through MMG (MMG backend); boundary-layer inflation reopened through a design pass for layers grown from a surface (boundary_layer, v17.23.0), the volume behind them filled bytetrahedralize; and quadrangulation reopened for CAD surfaces only (recombination ofmesh_cad_surface's own triangles by a matching, v17.23.0), field-aligned remeshing through installed programs (remesh_quads). A native constrained tetrahedralisation stays out:orient3dandinsphereexist and the Delaunay kernel is short (Bowyer 1981, Watson 1981), but boundary recovery with Steiner points is where TetGen and the exact constructions spend their complexity (George, Hecht and Saltel 1991; Si and Gärtner 2005; Shewchuk 2008; Si 2015; Diazzi et al. 2023). Reopen it when a design pass shows a native recovery on the core's predicates within a stated size, measured againsttetrahedralize's probes and its Thingi10K sample. Two exceptions are in scope. Surface triangulation of a CAD model is native since v17.13.0 (mesh_cad_surface): the robust predicates it needs are Shewchuk's, public domain and a few hundred lines, and a planar constrained Delaunay triangulation per face is small enough to own; this reverses the earlier decision that dropped the queued Delaunay item, for surfaces and the plane only. Lattice generation is the other:voxelize,octree,remesh_volume's isosurface stuffing and the octree volume mesheroctree_mesh(stuffed tetrahedra and the all-hexahedral octree dual, uniform or graded, plan) are index arithmetic over a signed distance on a lattice, followed by projection and untangling, with no predicate to make robust.A declared tolerance deciding degeneracy in
mesh_cad_surface—model_toleranceswidens where uses start, how far corners and uses may lie apart and which trims pair, but whether an edge is degenerate (a pole) stays on the global tolerance: an edge shorter than a generous declared tolerance is real geometry, and collapsing it would change the model rather than accept its gaps (item 21).What mesh generation leaves, by decision — recorded when roadmap §6 closed (October 2026); mesh generation and its measured-and-parked notes hold the numbers:
- No small-angle bound stated on a curved face. Corner lopping's bound (Pav and Walkington 2005) is the plane's; on a curved face
lop_cornersreports the angles it reaches and termination staysmax_pointsand the insertion-radius declines. Stating one would need the paper's argument redone in the surface's metric. - A pinched boundary stays declined by the flattening of 6.4.4: splitting the pinch into one vertex per wedge makes the coarse mesh manifold, but the face's loops pair the pinch's edges otherwise than the wedges do, so each loop would have to be re-stitched through both copies.
- Field-aligned quads stay with backends. Recombination of the mesher's own triangles leaves its worst quads at poles (0.12 on the sphere at
h = 0.1); field-aligned remeshing is Instant Meshes (BSD-3), QuadriFlow (MIT) or QuadWild (GPL, excluded), each a library of its own. Since v18.0.0 that pattern is the provider registry, andremesh_quadsruns an installed QuadriFlow or Instant Meshes as a separate process (0.25 to 0.58 on the same sphere, by seed); a linked add-on wheel extracting their solvers stays unqueued until a user needs them in the wheels or the browser. The core's cross field and quad layout (detail/cross_field,detail/quad_layout) stayfit_cad's. - Curved boundary layers: linearize first.
boundary_layertakes linear triangles and quads; atriangle6orquad9surface is linearized first. Growing curved layers needs a Bernstein bound on a prism's and a hexahedron's Jacobian (the tensor bounddetail/curved_quadhas for quads, raised a dimension) to keep "no cell is written folded" true, and its mid-edge nodes' directions; no model has asked. - Mesh-generation corpora with non-commercial terms stay out. The Fusion 360 Gallery (Willis et al. 2021) permits use for non-commercial research only and asks its users to indemnify Autodesk, which an MIT project's published measurements cannot accept.
- No numpy twin of
mesh_cad_surface,triangulate_polygon_2dortetrahedralize: a second implementation of the predicates and the triangulation could not stay byte-identical, the precedent ofremesh_volume. - Healing the model. Sewing gaps, removing sliver faces or rebuilding pcurves changes the CAD model, which is the CAD kernel's work;
mesh_cad_surfacemeshes around defects without writing them back. Narrowed in November 2026: the CAD kernel is reachable through the provider registry, so healing at read time, before the B-rep reaches the mesher, is the STEP and IGES readers'healoption (STEP reading);mesh_cad_surfaceitself still never heals. - Code from Triangle, TetGen, Gmsh, CGAL's GPL packages or Blossom V, under any route other than a separate process: their licences (THIRD_PARTY_NOTICES). Geogram's constrained tetrahedralisation is in that list too: it creates TetGen's AGPL Delaunay kernel. The separate-process route is
tetrahedralize(backend="tetgen")andbackend="gmsh"(external meshers: installed executables behind named tet-mesher providers, never linked), and the rule keeps the gmsh Python API, which runs in-process, out of the interop bridges (4.10). - A native parallel Delaunay kernel in the style of HXT (Marot, Pellerin and Remacle 2019): it is the fast path of a 3-D mesher the core does not have, and the 2-D triangulation of one face is small enough that faces in parallel already use every thread on a real model.
- Advancing-front surface meshing as a second native mesher (Lo 1985, Peraire et al. 1987, Löhner and Parikh 1988, Schöberl 1997): smoother gradation, but its robustness rests on the front's intersection tests that the Delaunay route avoids, and two native meshers would double what must stay byte-identical. Frontal point placement inside the Delaunay mesher (Rebay 1993, Tristano, Owen and Canann 1998) is the alternative if the varying metric's circumcentres leave triangles poorly aligned on strongly stretched faces.
- No small-angle bound stated on a curved face. Corner lopping's bound (Pav and Walkington 2005) is the plane's; on a curved face
Contact mechanics itself — penalty or mortar assembly, friction and contact search in time need a live solver. meshio++ detects and exports the interface of one snapshot (interfaces and contact); the contact algorithm belongs in the application that owns the solver.
MMG in the base wheel and in the native CLI executables — it is LGPL-3.0 (GPL-3.0 with the ELAS that
moveneeds) and an external install, so the native backend is linked dynamically into builds that opt in, and its operations throw by name where it is absent (MMG backend). v18.0.0 narrowed this for Python: themeshioplusplus-mmgadd-on wheel ships it, built without ELAS, beside the MIT base wheel (installation); the same split for the native CLI, a plugin beside the executable, is queued as 4.8.1. Its WebAssembly half was lifted for the full artifact when roadmap 3.1.8 closed (October 2026): MMG is linked there statically, without ELAS, under the LGPL-3.0 relinking terms the artifact's notices state (WebAssembly); the lean artifact keeps none.What the full WebAssembly build leaves, by decision — recorded when roadmap §5 closed (October 2026); fit CAD's measured and parked notes hold the numbers and the reopening conditions:
- Constraints beyond the coaxial, right-angle and equal-radius ones. Parallel but offset axes are different surfaces, and a bolt circle or a plane at another angle needs the pattern known to the pass, so
constrain_primitivesstays exact only where it is exact. - G1 at valence 5 and 6 beyond the sweeps' reach. The refine-before-discard step of the patch network brings the largest G1 angle down on the Catmull-Clark fixtures (limitations); a construction that holds G1 exactly there (Loop and Schaefer's full scheme, or G-splines) is not implemented.
- Genus-one-and-more homology cuts. The layout's pieces are triangle groups, so a cut along both sides of a loop has no representation; that is a change to the layout's partition, not a bounded experiment.
- THB stays experimental, behind its knot-line fallback: a rung whose hierarchical fit misses its target falls back to knot lines, so
"thb"never leaves as facets a patch"knots"fits.
- Constraints beyond the coaxial, right-angle and equal-radius ones. Parallel but offset axes are different surfaces, and a bolt circle or a plane at another angle needs the pattern known to the pass, so
KaHIP in the lean WASM build, ADIOS2 and TecIO in either WASM build — KaHIP ships only in the full artifact (serial kaffpa, built by
build-wasm-deps.sh --full; a graph partitioner would bloat every consumer's bundle, so the lean pair keeps the SFC method with"kahip"throwing by name as the contract); no one has made a wasm build of ADIOS2 (vtx); TecIO's redistribution terms keepszpltout. Each reports by name in the lean and the full artifact alike (what stays out).zstd/lz4 codecs in the lean WASM build — the lean artifact keeps zlib as its only block codec; the full one (v17.2.0) links zstd, lz4 and bzip2. A zstd-compressed VTK XML file is a meshio++ extension VTK itself has no compressor for, so vtk.js does not read it; the second reader of a zstd file is meshio++'s native and Python ones. Memory mapping and the Kokkos backend under Emscripten — CMake refuses the Kokkos backend under Emscripten, and there is nothing to map inside MEMFS.
Single-file output for DOLFIN, TetGen and EnSight from WASM, or lifting DOLFIN's simplicial restriction — both are facts of the formats;
writeMesh's written-paths return value (v11.2.0) is the fix for the bookkeeping they cause.NURBS patches and trims as a second entity kind inside
Mesh— decided with the isogeometric layer (v16.43.0, findings): a model (NurbsInfo,BrepInfo) stays a side channel beside the mesh, with its own evaluate, refine and sample operations, because a first-class entity kind needs patches, control nets, weights, knots and trimming curves inMesh, every mesh backend (MESHIO, NATIVE, KRATOS), every binding and the C++ ABI: a major version for a gain the three operations already give. Reopen it when an operation must act on patches and trims as mesh entities (mixed IGA and FEM assembly in one model, for instance, or a patch-awarepartition), and start from the spike's list of what the side channel cannot do: noMeshoperation sees a patch, the model does not travel through the pipeline, and an MFEM model refines every direction alike.Polyhedron blocks as a WASM gap —
vtu,ensight,cgns,medandopenfoamall write them from WASM; a consumer that cannot is constrained by its own data model.Performance work measured or decided against — recorded when the roadmap's performance section closed (October 2026), and not re-proposed when it reopened as §5 in November 2026; a new performance change passes the gates of landing a performance change:
- The serial phases kept for determinism: the keep-first weld loop (chains make it order-dependent), the union sweeps of
splitandcompute_normals(cheap, and their partitions are order-independent anyway), andremesh's energy sweep (every accepted move changes what the next test reads; it has no numpy twin because a near-tie decided differently yields a different clustering). - A facet table cached across pipeline steps — measured (benchmarks): in a
SmooththenExtractSurfacechain a cache saves at most one table build, about 3.6% of the chain on SEQ at tier L with the default ten iterations, at most 10% on four OpenMP threads and about 20% with one iteration.smoothandextract_surfacedo not share a record structure, so it would also need a refactor, and a cache on theMeshis a Tier A layout change. Reopen it for a workload that chains several facet readers after cheap topology-preserving steps; thepipeline_smooth_surfacerow measures it. - Link-time optimisation by default — measured (benchmarks), November 2026: identical output and binaries a third smaller, but the instruction counts move both ways (
read_vtk_ascii+40%,gradient+14%,decimate−6%), so it is the opt-inMESHIOPLUSPLUS_ENABLE_LTOrather than the default. - Explicit SIMD intrinsics or
-marchflags — portability across wheels, WASM and the release binaries is worth more than the scalar kernels cost; revisit only if the harness shows a kernel dominating. Narrowed in November 2026: the decision stands for flags and for intrinsics compiled unconditionally; variants selected at run time inside one portable binary, for kernels whose vector form is bit-identical, are to be measured as 5.15. - Kokkos device execution — every
parallel_forbody captures host pointers (parallel.hpp); the GPU route is the DLPack/CuPy handoff (GPU handoff). - An occupancy pyramid over the distance query's bucket grid (formerly roadmap 5.3.1) — measured in November 2026 and not taken: descending the grid's occupancy at coarser block sizes, best first with Euclidean bounds, was 2 to 3 times faster than the Chebyshev-shell search at tier L, but a bounding-volume hierarchy of the triangles (formerly 5.3.2, which had been decided against because its visit order was thought to make the equidistant-triangle tiebreak observable, a reason the total order on (distance², triangle id) answers) was a further 2 to 4 times faster with its build included, on uniform and graded targets alike, and replaced the grid in ABI 40 (benchmarks). A cell's bound is the cell's, so a point far from the surface still opens every bucket of a patch around its nearest one; a box is as tight as its triangles.
- Tuning the pure-Python fallback readers — the fix is making the C++ path accept the file, not a faster fallback;
_decimate.py's heap-based twin is deleted oncedecimateaccepts its inputs, not optimised. - Dropping the copy in
transform—transformcopies the connectivity and data it does not change (throughtransform_owned_copy, as 17 operation files copy throughclone_mesh,clone_geometryorquality_clone_mesh), and the Python twin copies too; that copy is what makes the result a new mesh that aliases nothing, so removing it is an API change, not an optimisation. Narrowed in November 2026: that holds fortransform; a second, explicitly in-place entry point changes no existing contract and is to be measured as 5.14. - Caching between the browser viewer's applies — every apply re-reads the staged file and replays the whole pipeline, which is what makes undo exact. Measured with the typed-array channel (benchmarks), the read is at most 2 to 25% of an apply that smooths and up to 69% of a quality-only apply on a large surface; a cached mesh or pipeline prefix would hold a second copy of the mesh in the 32-bit heap MEMFS already shares, and make undo depend on invalidation. The heap's size is the half of this a higher ceiling or a 64-bit build would change (5.13); measure again there before reopening.
- The serial phases kept for determinism: the keep-first weld loop (chains make it order-dependent), the union sweeps of
Code from AGPL-3.0 scan tools — ScanRuler and its STEP importer meshStep are AGPL-3.0, which this MIT library cannot take in any form: no copying, porting, translating or paraphrasing of their code, tests, shaders, guides or data. The scan metrology section (then §7, closed November 2026) was listed from their published feature descriptions only; an agent implementing an item of that kind (9.9 is one) works from the literature it cites and from meshio++'s own kernels, does not open their source, and does not use their output files as fixtures. The same rule as for Triangle, TetGen and Gmsh above, without the separate-process exception, since nothing there needs to run them.
Booleans of a scan with fitted solids — subtracting a measured cylinder from a scan, or adding a modelled body to it (roadmap 7.8.7, closed November 2026), waits on surface booleans, which a spike measured and kept out: exact in topology, but rounding its output to doubles left most curved results self-intersecting and coplanar contact unhandled (findings). A fitted solid reaches a CAD kernel as STEP (
write_step_elements), where the boolean is the kernel's. If a boolean backend passes the spike's criteria (9.1), this is its first use and the entry is removed.An interactive parametric CAD modeller — a sketch constraint solver, a feature history with rebuilds and rollback, face fingerprints that survive upstream edits, thread and insert catalogues: a CAD application, not a mesh library. meshio++ fits and writes geometry (
fit_cad, 7.6.5) and may build a solid from a scripted list of profile features through the CAD provider (7.7.4); editing that solid belongs to the CAD tool that opens the STEP file.Terminal rendering stays software, dependency-free and core-only — no GPU, no windowing stack, no new dependency and no audio library; it is a preview, not a replacement for Polyscope or vtk.js, and does not try to match their fidelity, lighting or picking. Python calls the core or raises by name (the
remesh_volumeprecedent: a NumPy edge-function rasterizer would be slow and double the surface that has to stay byte-identical). Vector output is unchanged: SVG, TikZ and glTF keep the painter's algorithm and their byte-pinned twins, and their colormap tables are only ever extended. The interactive loop is not a library feature (raw mode, signals and the event loop live in the CLI layer; the library exposes frames and encoders, never a tty, and WASM and the C API carry frames and text only).--followpolls modification times and does not use inotify or kqueue. The soundtrack is generated, played by an external player and silent unless asked for, in CI and over SSH. See terminal rendering.
Suggested sequencing
Open work only; what shipped is in CHANGELOG.md.
- Correctness debts (§1) — the refusal of 1.1.1 first: it is days of work and ends a silent misread. One thing elsewhere has the same urgency though it is not data loss: the licence texts missing from the published MMG and Netgen add-on wheels (4.8.1.1).
- Format reach (§2) — the checks against the vendor tools, as licences or files the tools wrote appear (recording a run). Depth before breadth in what is new, smallest first: the series writers that only need wiring (2.3.4: MED, UNV, then Gmsh, Tecplot, EnSight, CGNS), writing the EnSight parts that were read (2.3.3.1), then CGNS's boundary conditions and structured zones (2.3.1.1, 2.3.1.3) and Exodus's globals (2.3.2.1). Of 2.4, the shared netCDF helper (2.4.3.1) and the ZIP container (2.4.5.1) are prerequisites worth having on their own; Fluent's case files (2.4.1) and the coastal trio (2.4.3) come first among the formats.
- The OSS-Fuzz submission (3.1) and registration (4.1) — calendar-bound, so start them early;
SECURITY.md(3.14.1) before the submission. Among the registries, conda-forge (4.1.1), Blender (4.1.4) and the container image (4.1.8) need no decision; CRAN (4.1.2) and Julia (4.1.3) need the build question settled first. The dataset manager's thumbnails (4.6) can run beside them. - Tooling that guards everything after it (§3) — in this order, each days of work: dependency updates and pinned actions (3.14.2, 3.14.3),
clang-formatand the documentation checks (3.8, 3.16), coverage on pull requests and Hypothesis in the ordinary legs (3.11.1, 3.11.2), the compiler cache (3.17.2), the warning option and its first count (3.6.1, 3.6.2), ThreadSanitizer (3.9.1), the C APIabidiffgate (3.13.1). Then the longer ones: warnings to zero (3.6.3), CodeQL (3.10.1), typed Python (3.7), the round-trip fuzz target (3.12.1, after 3.1), the Clang-Tidy stages (3.5) and the free-threaded wheel (3.15.1, after 3.9.1). Code cleaning (3.4) runs alongside everything, in the order SonarCloud's duplication report gives. - Performance (§5) — the missing rows first (5.1.2), since every other item shows its numbers on them; then the wheels' two checks (5.2), the WASM bulk copy with its benchmark (5.9), raising the browser's memory ceiling (5.13.1), and the measurements that can each close their item (5.12, 5.14, 5.15);
decimate_volume(5.1.4),optimize_volume(5.5.2) and the sequence reads (5.11) by measured payoff. - Split Python packages on Windows (4.7) and the CLI plugins (4.8) — the licence texts (4.8.1.1) at once; the static-runtime measurement (4.8.1.2) decides the shape of both the plugin archive and the Windows CLI flavour (4.7.2); the MSVC library recipe (4.7.1) is independent.
- Bindings and front ends — Rust (4.2) with the C-header parity guard, which Julia and R gain from too; then .NET (4.11). The Jupyter widget (4.12) and the VS Code preview (4.13) share the viewer's build and the rebuild of its embedded page (7.3). Interop phases 2 and 3 (4.3, 4.10) are days each.
- The chatbot (4.4) and the embeddable MCP server (4.9) — the schema generator (4.4.2) first, since both need it; then 4.9's design pass before any native code, since it decides whether the Python server's sandbox and sanitizer move into the core.
- Code duplication (§6) — no open item; rerun
tools/dup_report.pyalongside everything, like 3.4, and list what it finds there. - SonarCloud findings (§7) — the next scan's counts first (7.1), then the coverage gate (7.2); the viewer's rebuild check (7.3) with the next WebAssembly build.
- Operations (§9) — the boolean backend's measurement first (9.1.1, 9.1.2), since it decides a non-goal and two parked features; then the small ones a user notices missing (mass properties 9.4, ray casting and point location 9.3, point-cloud conditioning 9.9, the IGA list 9.8), then subdivision and elevation (9.2, 9.5), the partitioner providers (9.7) and repeated green refinement (9.6.1). Only 9.10.2 and 9.6.2 wait on a decision.
- Data model (§8) — design passes only until one says otherwise. The two whose recommended form is a convention and days of work, and which close losses already listed on format pages: complex pairs (8.1) and the structured annotation (8.6). Strings (8.2) before hierarchy (8.5), which uses them. Integration-point data (8.3) is the largest and the one most formats wait on (2.3.6.1, MED ELGA, the result readers); side data (8.4) unblocks 2.3.1.4 and 2.3.2.3. The format items that wait on each say so.