Selective reads and read_metadata
Reading a mesh file is normally all-or-nothing: every data array is decoded whether or not you want it. Two options narrow that, a third picks which time step to decode, and a fourth call skips the arrays entirely.
All are additive. Default read() behaviour is unchanged.

Reading part of a file
import meshioplusplus
mesh = meshioplusplus.read("big.vtu") # everything (default)
mesh = meshioplusplus.read("big.vtu", points_only=True) # geometry, no data arrays
mesh = meshioplusplus.read("big.vtu", arrays=["u", "p"]) # only these data arrayspoints_only=Truekeeps points and connectivity — it narrows data, not topology, so the result is still a usable mesh you can convert or render.arrays=None(the default) reads every array;arrays=[]reads none. That distinction is deliberate and is preserved down to the C ABI.- Names not present in the file are ignored rather than raising, so the same call works across a directory of meshes that do not all carry the same fields.
Picking a time step
Formats carrying a time series hold every array once per step, but a Mesh holds one. Before time_step existed, every such reader silently took the first — correct for the overwhelmingly common single-step file, and quietly wrong for the rest.
mesh = meshioplusplus.read("run.exo") # step 0, as always
mesh = meshioplusplus.read("run.exo", time_step=2) # the third step
mesh = meshioplusplus.read("run.exo", time_step=-1) # the last step
meshioplusplus.read_metadata("run.exo")["time_values"] # [0.0, 0.5, 1.0]0(the default) is the first step, so existing behaviour is bit-for-bit unchanged.- Negative values count from the end.
-1means "the final state of the simulation", which is otherwise unsayable without knowing the step count up front. - Out of range is an error naming the available count, never a silent clamp — handing back step 0 for a request of step 7 is exactly the failure this option removes.
- Unlike
points_only/arrays, this is not a narrowing option: no caller-side filter can recover a step that was never read. So a format whose reader has no time concept raises rather than quietly returning the first step. read_metadata(...)["time_values"]reports the recorded times, so a request is checkable before it is issued. It is always present — empty for a format with no time concept — solen(meta["time_values"])needs no key test.
Currently honoured by exodus, xdmf (temporal collections — the counterpart to XdmfTimeSeriesWriter, see XDMF time series; the C++ reader resolves the collection structurally rather than running an XInclude/XPointer pass, and read_metadata's time_values come off each step's <Time Value> attribute without touching a payload), med (a CHA field's (NDT, NOR) step subgroups, whose zero-padded group names sort into step order), cgns (BaseIterativeData_t/ZoneIterativeData_t: mTimeStep resolves against NumberOfSteps and picks the one FlowSolution_t its FlowSolutionPointers names for that step, instead of reading every one), tecplot (every ZONE's SOLUTIONTIME/STRANDID, not just the first), gmsh (4.1 only; every $NodeData/$ElementData section's own real-tag time value, not just the first per name), ensight (.case TIME/VARIABLE: one file per step, templated by mTimeStep into filename start number: + step × filename increment:) and openfoam (<case>/<time>/<field> dictionaries: mTimeStep resolves against the case root's numeric-named time directories, sorted ascending; arrays selects which of that directory's field files to attach). time_step selection on MED dates to v9.9.0; on CGNS, Tecplot, Gmsh and EnSight, time_step selection and read_metadata's time_values are both new in v11.3.0 (read_cgns_metadata/read_tecplot_metadata/read_gmsh_metadata/read_ensight_metadata, roadmap §1 tier B1) rather than a full-read fallback or "takes the first step/zone/section" default — for EnSight, any variable reading is new; the format was geometry-only before. On OpenFOAM, both are new in v11.4.0 (read_openfoam_metadata, roadmap §1 tier B2) — the plain read() (no time_step/arrays at all) now attaches time-zero's fields by default too, where the format previously never carried point_data/cell_data at all. A multi-step MED field used to fail the read outright unless there was a Python fallback to defer to; a transient CGNS file used to silently concatenate every FlowSolution_t's arrays under whichever one's array name won last; a transient Tecplot file always read only its first ZONE, with every later one silently discarded; a transient Gmsh file always kept the first $NodeData/$ElementData section per name, silently discarding every later step of the same field; a transient EnSight .case file's VARIABLE section was ignored outright; OpenFOAM's time-directory fields were never read at all. vtkhdf joined in v14.0.0: Steps/Values supplies read_metadata's time_values without touching the geometry, and time_step=k reads only step k's slice of each flat array through the Steps offset tables (see VTKHDF). pvd joined in v15.0.0: the steps are the distinct timestep= values of the index, so read_metadata's time_values needs no piece opened, and time_step=k reads only that step's entries (see PVD).
Picking a piece
A partitioned file holds several pieces — VTKHDF partitions or the blocks of a composite — that the historical read merges into one mesh, with one cell region per piece. piece keeps one piece instead; like time_step it cannot be emulated after the fact, so it reaches a reader that understands it or fails loudly.
mesh = meshioplusplus.read("case.vtkhdf") # every piece merged, regions piece_0, piece_1, ...
mesh = meshioplusplus.read("case.vtkhdf", piece=1) # the second piece alone (no regions)
mesh = meshioplusplus.read("case.vtkhdf", piece=-1) # the last pieceOut of range is an error naming the piece count, never a clamp, and a format whose reader has no pieces raises rather than returning the merged mesh. The switch is a value plus a flag (ReadOptions::mPiece / mPieceSet, mio_read_opts.piece / piece_set) rather than a -1 sentinel: a hand-zeroed options struct must merge, not silently ask for piece 0 alone. ReadOptions::ResolvePiece(n) resolves a request against a piece count. Currently honoured by vtkhdf, by pvtu and pvtp (one <Piece> of the index; v15.0.0), by elmer (one part.n.* of an ElmerGrid partitioning.N directory, halo copies included; v16.2.0, see Elmer) and by pvd (one part of the chosen step: timestep= picks the step with time_step, part= the piece within it, so the two are orthogonal). A file with a single piece reads as that piece either way.
Dropping ghost cells
A partitioned .pvtu, .pvtp or a .pvd of them can carry a halo: cells another part owns, flagged in a vtkGhostType array so a piece is self-contained (partition(..., ghost_layers=N) writes one). Reading keeps them by default, because a reader must not silently discard data and keeping is what makes read → write round-trip. ghosts="drop" removes every cell with any vtkGhostType bit set, and the points only those cells used, from each piece before merging, and then the ghost arrays, which reconstructs the partition of unity:
mesh = meshioplusplus.read("case.pvtu") # halo kept: more cells than the original
mesh = meshioplusplus.read("case.pvtu", ghosts="drop") # the original cellsThe switch is ReadOptions::mGhosts (a GhostPolicy, Keep by default and for a zero-initialized struct) and mio_read_opts.drop_ghosts. It is deliberately not in the class time_step and piece are in: those cannot be emulated after a read, so they must reach a reader that understands them or fail, whereas a file with no halo is already the answer "drop" asks for. Like lenient, it is honoured by the readers that have the concept (pvtu, pvtp, pvd, which forwards it to every entry) and ignored by all the others. It is not an Input.Options key of a pipeline document, any more than piece is.
Summarizing without loading
meta = meshioplusplus.read_metadata("big.vtu")
meta["num_points"] # 1_048_576
meta["cell_blocks"] # [{"type": "tetra", "num_cells": 5_000_000, ...}]
meta["point_data_names"] # ["p", "u"]
meta["fell_back_to_full_read"] # False -> the summary really was cheapbbox_min/bbox_max are absent rather than None when no bounding box was computed, so "not computed" cannot be mistaken for a real box at the origin.
Which formats are actually faster
| Format | Selective reads | read_metadata | time_step |
|---|---|---|---|
| XDMF | native | native, and genuinely O(1) | ✗ (takes the first step) |
| VTU / VTP | native | native | n/a |
| PVTU / PVTP | native (arrays, piece), forwarded to every piece | native, the sum of the pieces' own summaries | n/a |
| PVD | native (arrays, time_step, piece), forwarded to every entry | native: every step's time from the index alone, plus step 0's pieces' summary | native |
| VTKHDF | native (arrays, time_step, piece) | native for a polyhedron-free UnstructuredGrid (steps from Steps/Values); otherwise a full read | native |
FEBio .xplt | native (arrays, time_step; only the chosen state's data is decoded) | falls back to a full read of the mesh, with every state's time from its header | ✅ |
Ansys .rst/.rth | native (arrays, time_step; only the chosen set's nodal solution is decoded) | falls back to a full read of the mesh, with every set's time from the time table | ✅ |
| Gmsh 4.1 | native | native | ✅ |
| Gmsh 2.2 | native | falls back to a full read | n/a |
| Exodus | read whole, then filtered | falls back to a full read, but reports time_values | ✅ |
| MED | read whole, then filtered | native, and genuinely O(1) | ✅ |
| CGNS | read whole, then filtered | native, and genuinely O(1) | ✅ |
| Tecplot | read whole, then filtered | native, and genuinely O(1) | ✅ |
| EnSight | read whole, then filtered | falls back to a full read, but reports time_values | ✅ |
| everything else | read whole, then filtered | falls back to a full read | ✗ |
reader_supports_options(fmt) reports whether a format has a native options-aware path at all. Note that "options-aware" is not one capability but several: Exodus is on that list for time_step, not because it narrows arrays natively, and MED/CGNS/Tecplot/Gmsh 4.1/EnSight are on it for time_step (MED also for lenient, see MED; EnSight's points_only/mDataArrays narrow which VARIABLE entries are read, not a payload within one).
A fallback is correct, just not fast, and it always says so via fell_back_to_full_read. A partial read that silently wasn't partial would be worse than no feature at all, so the flag is exposed on every binding surface.
How much faster, honestly
- XDMF is the best case: every
<DataItem>declares its shape in aDimensionsattribute, so counts are exact without touching any payload — and on the HDF path without opening the sibling.h5at all. - VTU/VTP: the file is still read and XML-parsed, because pugixml always materializes PCDATA. What is skipped is base64 decoding (parallel and decoded straight into the target array since v16.20.0, and often the larger half), decompression, allocation and byte-swapping. Expect a solid multiple, not an asymptotic change. The appended data format (
WriteEncoding::RawAppended,--appended; see VTU) is read too, in one pass since v16.21.0. - Gmsh 4.1 groups elements into typed blocks whose headers carry the type and count, so the summary walks block headers and skips each payload — by exact byte arithmetic for binary, line counts for ascii. Measured ~4× (ascii) and ~8× (binary) against a full read. Unwanted
$NodeData/$ElementDatabodies are skipped wholesale; since v11.3.0 the summary also reports every section's own time value (time_values, deduplicated names), andtime_stepon a real read picks the one section per name matching the resolved time instead of always the first. - Gmsh 2.2 stores a type on every element, so there is no cheap summary to be had; it declines and falls back rather than pretending. That is the honest answer for the format, not an omission.
Command line
meshioplusplus info --fast big.vtu # summarize from the header
meshioplusplus convert --points-only in.vtu out.vtu
meshioplusplus convert --arrays u,p in.vtu out.vtu
meshioplusplus convert --time-step=-1 run.exo last.vtu # negatives need the = form
meshioplusplus convert --piece=1 case.vtkhdf part.vtu # one partition instead of the merged mesh
meshioplusplus convert --drop-ghosts case.pvtu whole.vtu # the halo removedinfo --fast prints (no header-only path for this format; the file was read in full) when the summary was not actually cheap, and Time steps: N [...] when the file records more than one (a single-step file gives you nothing to choose). --points-only/--arrays cannot be combined with -s/-d, which operate on exactly the arrays that were skipped.
Both CLIs — the Python one and the native binary — behave identically.
Other languages
mio_read_opts opts;
mio_read_opts_init(&opts); /* always: fields added later default sensibly */
opts.points_only = 1;
opts.time_step = -1; /* the last step; 0 (the default) is the first */
opts.piece = 1; /* one piece of a partitioned file ... */
opts.piece_set = 1; /* ... only when piece_set is nonzero (0 merges every piece) */
opts.drop_ghosts = 1; /* remove the halo of a .pvtu/.pvtp/.pvd; other readers ignore it */
mio_mesh* mesh = mio_read_ex("big.vtu", "vtu", &opts);
mio_read_metadata* meta = mio_read_metadata_create("big.vtu", NULL);
int64_t n = mio_read_metadata_num_points(meta);
mio_read_metadata_free(meta);mio_read is unchanged. mio_read_opts is permanent ABI: it carries reserved capacity and may only grow additively.
call m%read('big.msh', points_only=.true.)
call m%read('run.exo', time_step=-1) ! the last step
! a piece: set opts%piece = 1 and opts%piece_set = 1 on a mio_read_opts_t
call m%read('case.pvtu', drop_ghosts=.true.) ! without the halo
meta = mio_read_metadata('big.msh')const mesh = m.readMeshSelective('big.vtu', { arrays: ['u'] });
const last = m.readMeshSelective('run.exo', { format: 'exodus', timeStep: -1 });
const part = m.readMeshSelective('case.vtkhdf', { piece: 1 }); // one partition, not the merged mesh
const whole = m.readMeshSelective('case.pvtu', { dropGhosts: true }); // without the halo
m.readMetadata('run.exo', 'exodus').timeValues; // [0, 0.5, 1]
const meta = m.readMetadata('big.vtu');
const withInfo = m.readMeshSelective('case.mdpa', { format: 'mdpa', lenient: true, info: true });
withInfo.info.skippedConstructs; // what `lenient` skipped, see belowlenient: skipping what a reader cannot represent
A reader that meets a construct it has no way to express throws ReadError naming it, so a caller with a Python fallback can take it and a caller without one at least learns what was in the file. That is the right default — but it makes formats whose optional sections are commonplace unreadable in practice for a caller that only wants the mesh.
ReadOptions::mLenient (C mio_read_opts.lenient, Fortran/Julia/R/WASM lenient, --lenient on the native CLI) turns those specific rejections into a warning plus a skip.
It is deliberately not "ignore all errors". It applies only where a reader can skip a construct and still return a correct mesh; a malformed file, a truncated block, a bad node reference or an unknown element type still throw, because continuing past those would hand back a mesh that is quietly wrong rather than merely incomplete.
Currently honoured by mdpa, vtkhdf and the svg/tikz drawings. For svg and tikz it is strict=False: a drawing that lacks the marker comment meshio++'s writers leave is read as a plain drawing (a warning says its scale and depth are unknown) instead of refused; every other rejection stays an error. For mdpa without a side channel it covers the Table, Geometries, Mesh, Constraints and non-empty sub-model-part data/table/geometry/constraint blocks; with MdpaInfo these are preserved, including geometry/constraint membership, and nothing is skipped. For vtkhdf it skips poly-vertex, poly-line and triangle-strip cells with a warning per run, dropping their cell_data rows with them. Every other reader ignores the flag. MdpaInfo::mSkippedConstructs records what was skipped, so "lenient" never means "silently lossy" — on WASM, readMeshSelective(path, {format: 'mdpa', lenient: true, info: true}).info.skippedConstructs reads it back (see doc/wasm.md's "Side channel (info)" section). See doc/formats/mdpa.md.
The Python read() deliberately does not take this parameter: mdpa's Python path is the pure-Python reference reader, which already accepts every construct the flag covers, so it would be a dead argument.