Skip to content

R ​

Exodus series and pipeline Version 2 ​

mio_exodus_series(path) returns an owning external pointer with a distinct tag and GC finalizer. mio_exodus_series_write_points_cells(series, mesh) fixes geometry, sets and attributes; mio_exodus_series_write_data(series, time, mesh) appends fields. _flush, _finalize, _num_steps, _finalized, _is_open and idempotent _release expose the lifecycle. Released/foreign pointers are R errors, never dereferenced; without netCDF construction fails naming the dependency. See Exodus.

The existing status/report pipeline functions accept Version 2 documents with auxiliary Inputs and terminal Output.Pattern; the binding requires no JSON parser package. Transient sequence documents remain Version 1.

Pipeline reports ​

mio_pipeline_run_file_report(path), mio_pipeline_run_json_report(text) and mio_sequence_pipeline_run_*_report return a JSON character scalar with steps and warnings, automatically releasing its native owner. Parsing with jsonlite::fromJSON is optional; no new binding dependency is required. Existing status-only APIs are unchanged. See pipeline reports.

MED named meshes ​

mio_med_mesh_names(path) enumerates file link order; mio_med_read_named(path, name, time_step=0L, lenient=FALSE) returns an owning mesh handle; mio_med_write_multi(path, meshes, names, version="4.1.0") writes a nonempty list of meshes with one unique name per mesh. HDF5 is required; ordinary nodal/element profiles expand with NaN fill. See MED.

mio_write(mesh, 'cloud.pcd', codec='lzf') selects PCD compressed binary without an optional codec library; explicit ASCII or non-PCD combinations fail. Generic writes also accept encoding and float_format (encoding = 'binary' on a .frd writes CalculiX's binary result layout). mio_write_gltf(mesh, 'colored.glb', color_by='temperature', cmap='turbo', up_axis='z', scale=0.001) exposes all native glTF options; component is 1-based or NULL for magnitude, and unset vmin/vmax select automatic bounds. Native reads also support XDMF2/3 with absolute DataItem references and Netgen names/periodic arrays; Netgen periodic arrays live in field_data and keep file node ids 1-based, while .vol.gz requires zlib.

XDMF series mio_xdmf_series_write_points_cells(series, mesh) now stores the mesh's fixed point/cell/side regions once with the shared topology. Existing step, flush and append calls retain them without new arguments. See shared named regions.

The shared native reader supports multiple pieces without welding and appended raw/base64 arrays in vtu, vtp, vts, vtr and vti, with UInt32/UInt64 headers and either byte order. VTP/VTS/VTR/VTI writers remain inline; optional codecs follow the C library build. Legacy vtk also reads structured points, structured grids and rectilinear grids in ASCII and big-endian binary through the existing read API. See formats.

meshio++ ships an R package, meshioplusplus, layered on the C API — the same flat C library the Fortran and Julia bindings sit on:

r
library(meshioplusplus)

m <- mio_read("bracket.msh")
m
#> <mio_mesh: 9231 points, 42145 cells in 3 block(s)>

surf <- mio_extract_surface(m)
mio_write(surf, "bracket_surface.vtu")
mio_release(m)

Every exported function is prefixed mio_, which keeps the package clear of base R names such as points(), stats(), split() and merge().

mio_read(path, format = "gmsh") reads non-periodic Gmsh 2.2, 4.0 and 4.1 through the native library. The 4.0 path accepts ASCII and binary with 4- or 8-byte producer counts. Periodic files use mio_read_with_info / mio_write_with_info; output remains 4.1 (gmsh) or 2.2 (gmsh22). Info-less reads refuse $Periodic rather than lose it. See Gmsh.

The package is MIT-licensed like the rest of meshio++. (The sibling Julia binding is deliberately not — see its page.)

Building and installing ​

The package binds the installed C library; it compiles only its own thin C shim.

sh
./build/configure.sh --c-api --build
cmake --install build/cpp-release --prefix /opt/meshioplusplus

Then make it discoverable, either through pkg-config (the relocatable .pc the C API already installs):

sh
export PKG_CONFIG_PATH=/opt/meshioplusplus/lib/pkgconfig

or by naming the prefix directly:

sh
export MESHIOPLUSPLUS_HOME=/opt/meshioplusplus

and install:

sh
R CMD INSTALL bindings/r/meshioplusplus

At run time the shared library must also be on the loader path (LD_LIBRARY_PATH on Linux, DYLD_LIBRARY_PATH on macOS). On Windows configure is not run: set MESHIOPLUSPLUS_HOME and put libmeshioplusplus.dll on PATH.

The package's configure script probes those two routes and generates src/Makevars from src/Makevars.in. That indirection is deliberate: a committed Makevars containing a $(shell ...) earns a non-portable flags NOTE from R CMD check --as-cran, while generating it from a .in is the pattern CRAN packages such as sf and xml2 use.

Array layout — column-major, no transpose ​

R matrices are column-major and the C core row-major, so

r
mio_points(m)                     # dim x num_points
mio_connectivity(m, i)            # nodes_per_cell x num_cells
mio_point_data(m, "displacement") # components x num_points

are the same layout as the C API's row-major (num_points, dim), (num_cells, nodes_per_cell) and (num_points, components). One memcpy and the shape is already right — nothing is transposed on either side, exactly as in the Fortran and Julia bindings.

The one deliberate exception is mio_transform(), whose 4×4 affine matrix is transposed on the way out: that is a mathematical object, not a mesh array.

R is copy-only ​

No zero-copy borrow in R

R vectors are R-managed, so the C API's zero-copy borrow does not survive into R without ALTREP machinery that is out of scope here. Every accessor in this package copies. That is a real difference from the Julia and Fortran bindings, which do hand out live views into the C++ core's buffers.

Because of that there is no _ptr accessor at all. The 0-based reader is named mio_connectivity_raw(), deliberately not mio_connectivity_ptr(), so nobody reads it as a borrow:

accessorcopies?node indices
mio_points(m)yesn/a
mio_connectivity(m, i)yes1-based
mio_connectivity_raw(m, i)yes0-based — the ABI's own

The ±1 shift happens in that one copy (dtype conversion and shift are fused into a single pass over the result), which is where the other two bindings put it too.

64-bit integers ​

R has no native 64-bit integer type. The int64 arrays the C API returns — connectivity, region entries, index maps, permutations — therefore arrive as double. That is exact to 2^53, far beyond any mesh this library will meet, and it avoids a hard dependency on bit64 for a theoretical case. It is a limitation of the R side, not of the ABI.

The dtype a data array was actually stored as is reported separately rather than silently lost:

r
attr(mio_point_data(m, "temperature"), "dtype")
#> [1] "float64"

Indexing ​

Cell-block indices, data-name indices, connectivity and region entries are all 1-based. Index maps and permutations are 1-based too, with the C API's -1 "pruned / absent" sentinel becoming 0 — never a valid 1-based index, so it stays unambiguous. That is verbatim the Fortran and Julia rule.

mio_partition_labels() is the exception: those are part ids, not indices, so they stay in 0:(nparts - 1).

Region entries are 1-based with one further exception (see doc/regions.md): for a "side" region the second row is a facet ordinal within the cell type, not a mesh index, so it is passed through unshifted.

Memory management ​

A mesh handle is an external pointer (R_MakeExternalPtr) with a registered finalizer calling mio_mesh_free, so it is released when garbage-collected. mio_release() frees it immediately and is idempotent; using a released handle raises a clean R error rather than crashing, and so does passing something that is not a mesh handle at all.

mio_refine() takes an optional cell selection: at most one of cells (global block-major, 1-based here), region (a cell region selects its cells, a point region every cell with any node in it; a side region is an error) and where_array + where_op + where_value, plus closure ("redgreen", local, or "propagate", which reaches the whole edge-connected component) and record_levels. With no selector every cell is refined. record_hierarchy attaches refine:cell_id/refine:parent_id — the persistent parent/child hierarchy a multigrid caller resolves across the sequence of meshes it keeps — and forces refine:entity (the multigrid prolongation stencil) to be attached even when the closure leaves no hanging node. See refine. Note that R's data setters always write Float64, so a predicate over an array built fresh in R still works — the comparison is numeric — but mio_split(by = "region")'s integer-tag restriction does not apply here.

Operations producing an opaque C result (mio_split(), mio_partition(), mio_reorder(), mio_refine(), mio_decimate(), mio_convert_cells(), mio_subdivide(), mio_agglomerate()) always transfer ownership of the mesh out of that result rather than returning a borrow into it, so a piece stays valid after the result is gone.

Error handling ​

Every failure becomes an R condition carrying the C API's own thread-local message via Rf_error(); a mio_status code never reaches R.

r
mio_read("/nope.vtu")
#> Error: meshio++: cannot open file '/nope.vtu'

Named regions ​

r
mio_add_region(m, "inlet", "point", c(1, 3, 5))
mio_add_region(m, "solid", "cell", c(1, 2), dim = 3L, tag = 17)
mio_add_region(m, "wall", "side", matrix(c(1, 0, 2, 2), nrow = 2))

for (r in mio_regions(m)) {
  cat(r$name, r$kind, ncol(r$entries), "entries\n")
}

The interface operations use the same named regions: mio_region_adjacency() extracts conforming shared facets, mio_find_interface() matches two Cell-region boundaries, mio_contact_pairs() projects a Point region to a master Cell region, and mio_split_interface() duplicates point fans along a Side region. These return copied arrays and/or an owned mesh; point and global cell ids are 1-based in R, while local facet/subfacet ordinals remain 0-based. The full signatures and examples are in Interfaces and contact.

Why plain .Call, not Rcpp ​

.Call with R's own C API keeps the dependency footprint at exactly zero — the package has no Imports and needs no C++ toolchain during R CMD check — and it matches the flat C ABI the rest of meshio++'s bindings sit on. The whole surface is scalars, vectors and matrices, where Rf_allocVector / Rf_allocMatrix plus a memcpy is all that is required; Rcpp would buy convenience this surface does not need.

Selective reads and time steps ​

Format inference is the shared registry's, the C API's: the extension picks the format, with seven exceptions: a .mesh file whose first line names an MFEM mesh is read as mfem rather than medit (v16.5.0), Z88's fixed file names (z88i1.txt, z88o2.txt, …) are z88 rather than xyz (v16.7.0), a .dat file that opens as an MSC Marc input deck is marc rather than tecplot (v16.8.0), a file named d3plot, which has no extension, is LS-DYNA's lsdyna_d3plot (v16.9.0), a file named like crashA001 (a stem, A and three or more digits, no extension) is an OpenRadioss animation file, radioss_anim (v16.11.0), a file named binout (or binout0000…) is LS-DYNA's lsdyna_binout, and one named like crashT01 (a stem, T and two digits, no extension) an OpenRadioss time-history file, radioss_th (both v16.12.0). The two content checks are for reading: a write goes by the extension alone, so a .dat is written as Tecplot and a .mesh as Medit whatever the file being replaced holds (v16.17.0), and a Marc deck is written by naming marc. Formats added to the registry (Patran, Femap and MFEM in v16.5.0; libMesh, Z88, Abaqus .fil and Radioss in v16.7.0; Marc's marc and marc_t19 and the ansys_rst_cyclic full rotor in v16.8.0; lsdyna_d3plot and nastran_op2 in v16.9.0; Tecplot's binary .plt in v16.10.0; radioss_anim, the libMesh writer and its .gz/.bz2 names in v16.11.0; lsdyna_binout and radioss_th in v16.12.0; vtx, DOLFINx's ADIOS2 .bp directories, and szplt, Tecplot SZL, in v16.13.0, when the library was built with ADIOS2 and TecIO; the radioss and marc writers in v16.17.0, the radioss_th, nastran_op2, lsdyna_binout and abaqus_fil writers after them) reach this binding with no code change.

mio_read() narrows what it materializes, and since v8.6.0 also picks which time step of a multi-step file to decode:

r
m <- mio_read("big.vtu", points_only = TRUE)

# 0 (the default) is the first step; negative counts from the end.
last <- mio_read("run.exo", time_step = -1)

meta <- mio_read_metadata("run.exo")
meta$time_values   # c(0, 0.5, 1) -- how many steps `time_step` may name

Out of range is an error naming the available count, never a silent clamp. meta$time_values has length 0 for a format with no time concept. Honoured by exodus; see Selective reads.

Transient (time-series) XDMF writing ​

mio_xdmf_series() is the write half of the above, and the one writer mio_write() cannot express: a series is a stateful multi-call object, so it gets its own handle rather than an argument. The grid goes out once and each solve appends a cheap step. See XDMF time series.

r
s <- mio_xdmf_series("simulation.xdmf")        # "HDF" by default
mio_xdmf_series_write_points_cells(s, mesh)    # the static grid, once
for (k in 0:9) {
  solve(mesh)
  mio_xdmf_series_write_data(s, k * dt, mesh)  # point_data/cell_data only
}
mio_xdmf_series_num_steps(s)
#> [1] 10
mio_xdmf_series_finalize(s)                    # release() would do this too
mio_xdmf_series_release(s)

data_format is "HDF" (the default; needs an HDF5-enabled library), "XML" (everything inline in the .xdmf) or "Binary"; gzip_level applies to "HDF" datasets only and is negative (no compression) by default. An unknown format, or "HDF" against a library built without HDF5, is an error carrying the C API's own message.

Two things worth knowing before reading the result back:

  • the .xdmf light data is buffered until the series is finalized, so the file is only readable after mio_xdmf_series_finalize() (or mio_xdmf_series_release(), which finalizes first);
  • a write failure during that implicit finalize cannot be reported from a GC finalizer at all, which is exactly why mio_xdmf_series_finalize() exists as a separate call. Prefer it, and release the handle before the directory it writes into goes away.

The handle is an external pointer with its own tag, so a mio_mesh and a mio_xdmf_series are never accepted for one another; mio_xdmf_series_release() is the idempotent deterministic free, and mio_xdmf_series_is_open() the predicate. Reading a finished series back is the ordinary mio_read(path, time_step = k).

Documented gaps ​

Sets↔data conversions are available as mio_sets_to_data(mesh, location, data_name=NULL, join_char="-", order=NULL) and mio_data_to_sets(mesh, location, key) for location="point" or "cell". Both return a new mesh. Labels are values, not indices: zero-based labels and -1 for uncovered rows are not shifted. Omit order for native region-name order, or list every set name once. See sets/data semantics.

These are gaps in the C ABI, shared with the Fortran and Julia bindings; the R package invents no workaround for any of them:

  • Four Python-only formats. pmsh, zarr, cae and usd (v10.35.0, the physics-ML data path) are registered in the Python layer only, not in the shared C++ dispatch registry, so this surface cannot read or write them. They are export targets for a training pipeline rather than interchange formats; see formats.

  • point / cell sets beyond regions never reach the C++ core at all;

  • the frozen pin mask of mio_smooth() and mio_decimate();

  • per-cell-type counts in mio_stats() — use mio_cell_block_types() with mio_cell_block_info();

  • ragged block connectivity — closed in v9.15.0: mio_polygon_block() / mio_polyhedron_block() read them as nested 1-based lists and mio_add_polygon_block() / mio_add_polyhedron_block() build them. mio_connectivity() still raises, since a ragged block has no matrix. See Polyhedra and ragged cells;

  • the combined data_manage — mio_data_drop() / mio_data_keep() / mio_data_rename() compose to the same effect;

  • Exodus provenance strings (qa_records / info_records): they ride the ExodusInfo side channel, which like MedInfo does not cross the flat ABI, so mesh$info has no counterpart here. Geometry, data, regions and time steps are unaffected.

As on the Julia page: gmsh does not currently round-trip named regions (the $PhysicalNames entry is written, but no physical tag is attached to an entity in $Elements, so a reader cannot rebuild the group). This is pre-existing meshio++ behaviour, reproducible from Python; abaqus round-trips regions correctly.

One further limitation is specific to this binding rather than the C ABI: the data setters always write Float64. mio_add_point_data(), mio_append_cell_data() and mio_add_field_data() copy through REALSXP regardless of the R vector's own storage mode, because R has no integer type reaching the C ABI here — the same reason 64-bit integers come back as double on the reading side. The practical consequence: mio_split(by = "region") needs a genuinely integer cell-data tag, so a tag array built fresh in R cannot drive it — only an integer tag already present in a read file (a gmsh physical group, an Exodus block id, an mio_isosurface()-produced iso:index, …) can. example/r/03_mesh_operations.ipynb's split demo uses by = "type" for exactly this reason.

Tests ​

sh
R CMD build bindings/r/meshioplusplus
R CMD check --as-cran meshioplusplus_*.tar.gz

with PKG_CONFIG_PATH and LD_LIBRARY_PATH pointed at the install prefix. The testthat suite mirrors the Julia one on the same deliberately non-square fixture, so a transposed mapping or a missed shift cannot cancel out.

Software rendering (v16.34.0) ​

A deterministic software rasterizer and its terminal, HTML and PNG encodings, with no display or GPU: see terminal rendering for what is drawn and every option. Every option is a named argument in ..., with the C mio_render_opts field's name; an unknown name is an error listing the ones that exist.

r
f <- mio_render(mesh, width = 800L, height = 600L, supersample = 2L,
                color_by = "temperature", colorbar = TRUE, clip = c(2, 98), isolines = 8L)
f$rgba        # integer array c(4, width, height); f$cell_ids is width x height, -1 for none
f$range       # c(vmin, vmax), or NULL when no field is mapped; f$notes the ticks and keys
rasterImage(mio_frame_array(f), 0, 0, 1, 1)   # numeric c(height, width, 4) in [0, 1]

cat(mio_render_text(mesh, cols = 80L, rows = 24L, encoding = "braille", color_by = "temperature"))
png <- mio_render_png(mesh, width = 800L, height = 600L)   # a raw vector
mio_write_snapshot("part.png", mesh)                       # .png .txt .ansi .html .cast

Cut-aways (v16.36.0). cutaway = c(px, py, pz, nx, ny, nz) (a point and the normal of the side kept), or a list of up to two such vectors, with cutaway_tint = mio_rgba(...), clips the geometry away and tints the back faces now in view: mio_render(mesh, view = "-x", cutaway = c(0.5, 0, 0, 1, 0, 0)).

Streamlines (v16.37.0). streamlines = "velocity" follows a vector point array over the mesh's cells, with stream_seeds (default 40), stream_length (model diagonals each way, default 0.5) and stream_color = mio_rgba(...): mio_render(m, streamlines = "velocity", stream_seeds = 60L). An unknown array or a seed count outside 1 to 10000 is an error naming it.

Colours are numbers 0xRRGGBBAA (mio_rgba(r, g, b, a)) or c(r, g, b[, a]); clip = c(2, NA) leaves the high end alone. The text is UTF-8 whatever the session's locale. mio_detect_color_depth(no_color, color_term, term) returns "truecolor", "256", "16" or "mono".

v15.4.0 additions ​

  • mio_compute_normals(mesh, point_normals = TRUE, cell_normals = FALSE, weight = "angle", split_angle = NULL, record_parent_ids = FALSE, region = "") — point and cell normals of a surface, written as normals (an (n, 3) point-data matrix; a cell-data array with cell_normals). split_angle = NULL gives one smooth normal per point; a number of degrees in [0, 180] duplicates points at creases so every point carries exactly one normal, appending the copies after the original points while cells keep their numbering. Returns a list of mesh, quality, num_isolated, num_undefined, num_degenerate, num_split_points and num_added_points. Never reorients: check quality$inconsistent_pairs. See normals.
  • mio_check_quality(mesh, require = "", max_inverted = 0, max_degenerate = 0) — the quality gate; a list of passed, the counts and summary.
  • mio_feature_edges(mesh, feature_angle = 30, feature = TRUE, boundary = TRUE, non_manifold = TRUE, inconsistent = TRUE, region = "") — the sharp, open, non-manifold and inconsistently wound edges as a line mesh; returns a list of mesh and the per-category counts. See feature edges.
  • mio_tessellate(mesh, levels = 2, curved = TRUE, fields = TRUE, record_stencil = FALSE) — isoparametric tessellation of triangle6/quad8/quad9/tetra10/hexahedron27 cells into triangles and tetrahedra: a list of mesh (with tessellate:source_point, tessellate:source_cell, tessellate:sub_index and, with record_stencil, tessellate:stencil/tessellate:weights) and the counts num_source_points, num_source_cells, num_curved_source_cells, num_pass_through_source_cells, num_points, num_cells, num_boundary_facets, num_interior_facets and num_facets_with_bad_count; see tessellation.
  • mio_segment(mesh, feature_angle = 30, normal_tolerance = 0, curvature_tolerance = 0, min_patch_cells = 1, region = "", prefix = "patch", method = "angle", tolerance = 1e-3) — smooth patches bounded by the feature edges (or, with method = "primitive", one per plane, cylinder, cone, sphere or torus): a list of mesh (with segment:patch, segment:adjacency and one cell region per patch), num_patches, num_merged, num_feature_edges and num_boundary_edges; see segment.
  • mio_fit_primitives(mesh, patch_data = "segment:patch", tolerance = 1e-3, relative = TRUE, min_inlier_fraction = 0.9, types = c("plane", "sphere", "cylinder", "cone", "torus"), max_iterations = 50) — a primitive fitted to each patch: a list of mesh (with fit:* cell data and fit:patch_* field data) and the per-type counts; see fit primitives.
  • mio_refine_patches(mesh, tolerance = 1e-3, relative = TRUE, normal_weight = 1, epsilon = 1e-3, feature_angle = 30, max_iterations = 10L, min_patch_cells = 3L, ransac_seed = NULL, types = NULL, allow_split = TRUE, allow_merge = TRUE, prefix = "patch") (v17.11.0) — refines the segment:patch labels by variational shape approximation, moving boundaries, splitting a freeform patch where seeded RANSAC finds a primitive inside it and merging neighbours one primitive explains: a list of mesh (with the patches, regions and fit:* data rewritten plus refine:* field data), num_patches, num_iterations, num_splits, num_merges, num_absorbed, num_primitive, num_freeform, num_facets, the absolute tolerance, explained_area_fraction, initial_explained_area_fraction, hausdorff_max, hausdorff_rms, initial_energy, energy and stop ("unchanged", "small_gain", "iteration_cap" or "energy_rose"). types = NULL allows every primitive. ransac_seed = NULL takes the default seed (the bytes of "meshio++", above 2^53); since R numbers are doubles, a seed given as a number is a whole number in [0, 2^64) taken exactly only below 2^53. See refine patches.
  • mio_iga_refine(info, subdivisions = 1, elevate = 0, sample_subdivisions = 0, trim_samples = 16, edges = TRUE, trim = TRUE), mio_iga_sample(info, sample_subdivisions = 0, …) and mio_iga_evaluate(info, patch, params, derivatives = 0, trim_samples = 16) act on the NURBS model of a mio_read_with_info() handle (MFEM NURBS, CAD JSON, STEP, IGES): the first two return list(mesh, info) (the sampled mesh and the model, for mio_write_with_info()), the last list(points, fields, derivatives, normals, inside) with params a (d, N) matrix and patch 1-based. See isogeometric layer.
  • mio_mesh_cad_surface(info, target_size, max_chordal_deviation = 0, min_points_per_edge = 0, tolerance = 0, record_face_ids = TRUE, edges = TRUE, match_unreferenced_trims = TRUE, strict = FALSE, max_points = 50000000, min_angle = 0, smooth_iterations = 0, smooth_method = c("laplacian", "odt"), order = 1, grading = 0, curvature_deviation = 0, cells_across_gaps = 0, pole_rings = 0, face_sizes = NULL, edge_sizes = NULL, background = NULL, background_array = "size", metric = c("mean", "varying"), virtual_edges = FALSE, reparam_condition_number = 0, model_tolerances = FALSE, virtual_faces = FALSE, lop_corners = FALSE, metric_background = NULL, metric_array = "", metric_error = 0) triangulates the faces of a cad_json, step or iges handle into one conforming surface mesh and returns a list with mesh, the counts, the extreme angles and edge lengths, max_edge_gap, face_status and face_reason (one entry per face) and warnings. min_angle refines to a smallest angle on the surface and smooth_iterations smooths; the list adds num_refinement_points, num_faces_below_bound, num_smoothed, min_input_angle_deg, angle_histogram (18 ten-degree bins) and corners, a data frame of the corners under 60 degrees and poles (1-based face and point). order 2 or 3 (v17.16.0) writes curved triangle6 or triangle10 elements whose nodes lie on the geometry, and num_curved_straightened counts those kept straight-sided. curvature_deviation and cells_across_gaps (v17.18.0) size the mesh by the model's curvature and gaps, grading bounds the size's growth and pole_rings grades the poles. face_sizes and edge_sizes (v17.19.0; a named vector or a two-column matrix of 1-based indices and sizes) and a background mesh with its point data background_array give sizes that join the same field, metric = "varying" meshes each face in its surface's own metric and virtual_edges skips slivers of edges; the list adds face_size_limits (per face, the 1-based edges holding it finer than its given size) and virtual_edges (each chain's 1-based edges). reparam_condition_number (v17.20.0) meshes a face whose parameters stretch more than that in a flattening of its coarse mesh, virtual_faces meshes a sliver face with its tangent neighbour, model_tolerances accepts the gaps the model declares and lop_corners lops acute corners (Pav and Walkington 2005) in place of concentric shells; a metric_background mesh whose 9-component point data metric_array is a field's Hessian, with the target interpolation error metric_error, sizes the triangles by an anisotropic metric (roadmap 6.10.2); the list adds the data frames reparametrised (face, condition_number, condition_number_disc, method, reason), merged_faces (sliver, host, width, merged, reason) , tolerance_uses (edge, declared, gap, where) and lopped_corners (face, point, angle_deg, lfs0, lop_radius, turning_deg, stated_bound_deg, num_forced, min_forced_deg, within_two_radii, guaranteed, reason), every index 1-based. See mesh generation from CAD.
  • mio_fit_cad(mesh, step_path = "", tolerance = 0, exact_edges = TRUE, types = c("plane", "sphere", "cylinder", "cone", "torus"), mesh_deflection = 0, max_demotion_rounds = 8, freeform = TRUE, freeform_degree = 3, freeform_max_control = 64, freeform_split = TRUE, fillets = TRUE, threads = TRUE, merge_faces = TRUE, backend = "auto", refine_segmentation = FALSE, max_refine_iterations = 10L, refine_epsilon = 1e-3, normal_weight = 1, allow_merge = TRUE, allow_split = TRUE, ransac_seed = NULL, refine_tolerance = 1e-3, relative_tolerance = TRUE, freeform_general = FALSE, freeform_tolerance = 0, freeform_smoothing = 1e-12, max_freeform_poles = 40L, max_freeform_iterations = 30L, max_freeform_splits = 2L, sew_tolerance = 0, extend_surfaces = FALSE, snap_junctions = FALSE, face_deviation = FALSE, freeform_mode = "legacy", freeform_parameterization = "auto", extraordinary_vertex = "g1x", local_refine = "knots", freeform_g1_tolerance_deg = 1, freeform_max_control_points = 400L, layout_target_patch_size = 0.1, layout_max_valence_deviation = 2L, resegment_rounds = 2L, layout_snap_to_features = TRUE, validate_by_surface_sampling = TRUE) — the fitted patches as a CAD solid with analytic faces (and, for the patches no primitive explains, rolling-ball fillet and blend faces with fillets, helical thread faces with threads and, with freeform, B-spline faces, a patch folding over every flattening split into pieces with freeform_split), written as STEP to step_path when given: a list of mesh (the solid re-meshed, with fit_cad:* cell data), valid, the volumes and their ratio, max_deviation, the faces by type (num_bspline, num_fillet and num_helical among them, and of those num_variable_fillet, num_blend and num_rounded_thread), num_vertex_blend and num_chamfer (v17.2.0) and, of the fillets and threads, num_conic_blend, num_tapered_thread and num_variable_pitch_thread, num_split_patches, num_demoted, the edge counts and backend. backend = "occt" needs a library built with OpenCASCADE, which mio_has_occt() reports, and fails naming -DMESHIOPLUSPLUS_WITH_OCCT=ON otherwise; "native" writes the fitted primitives as analytic STEP faces with no CAD kernel; "auto" picks OpenCASCADE when present. Since v17.11.0, refine_segmentation = TRUE first refines the patches as mio_refine_patches() does (through mio_fit_cad2), with refine_tolerance (relative while relative_tolerance), normal_weight, refine_epsilon, max_refine_iterations, allow_split, allow_merge and ransac_seed (NULL the default seed, a number exact below 2^53); the list adds refined, refinement_kept (whether the solid was built from the refined patches), refine_iterations, num_refine_splits, num_refine_merges, initial_explained_area_fraction and explained_area_fraction, zero when the refinement did not run. freeform_general = TRUE tries a patch the plane and cylinder freeform fit leaves unexplained as a general B-spline face (freeform_tolerance, freeform_smoothing, max_freeform_poles, max_freeform_iterations, max_freeform_splits), and, OpenCASCADE only, sew_tolerance (0: the tolerance) sets the sewing tolerance, extend_surfaces extends freeform surfaces past their patches, snap_junctions places the corners where analytic faces meet by least squares and face_deviation measures each face against the skin (the fit_cad:face_hausdorff / fit_cad:face_rms field data); the list always adds closed, num_freeform_general, num_freeform_refinements, avg_freeform_poles, num_extended_edges, num_snapped_junctions, hausdorff_max and hausdorff_rms (NaN when not measured). Since v17.12.0 the freeform modes go through mio_fit_cad3: freeform_mode ("legacy", "adaptive", "network" or "auto"), freeform_parameterization ("auto", "projection", "lscm", "harmonic"), extraordinary_vertex ("g0", "g1", "g1x", "g1xx") and local_refine ("knots", "thb") as strings, with the layout and budget arguments above; the list always adds brep_checked, freeform_mode, facet_area_fraction (the share of the area left on facet faces), num_freeform_adaptive, num_network_patches, num_network_edges, num_extraordinary_vertices, num_resegmented, num_escalations, max_g1_angle_deg, max_freeform_distance, rms_freeform_distance and num_face_records, and outside "legacy" the mesh carries the face records as fit_cad:face_table field data (one row per patch tried as a surface). See fit CAD.
  • mio_region_adjacency(mesh, regions = NULL) — conforming shared facets between Cell regions; when fewer than two are selected or present, multiple cell blocks are used as groups. See region adjacency.
  • mio_hausdorff_distance(a, b, face_samples = 0, region_a = "", region_b = "") — a list of distance, the one-sided maxima with means and RMS, the sample counts and the worst points. See Hausdorff distance.
  • mio_edit_regions(mesh, op, inputs, output = "", kind = NULL, dim = NULL, tag = NULL, keep_inputs = TRUE) and mio_remove_region(mesh, index) (1-based). See editing regions.
  • mio_match_periodic_nodes(mesh, slave, master, matrix = NULL, translate = NULL, atol = 1e-8, require_complete = TRUE) — a list of 1-based slave / master ids, unmatched, num_fixed and max_residual; matrix is an R 4x4 (M %*% c(p, 1)). See periodic node pairs.

v10.9.0 additions ​

  • mio_hessian(mesh, array, method = "green-gauss", location = "cell", output = "", overwrite = FALSE) — the Hessian (second derivative) of a scalar point-data field, mio_gradient()'s companion one order further. A composition of TWO mio_gradient() calls, not a new numerical kernel: the field is differentiated once (point location), then that (n, 3) gradient is differentiated again with the default "gradient" operator, producing (n, 9) — the flattened row-major 3x3 Hessian, H[i,j] at index i*3+j. method is forwarded to BOTH internal passes. Returns a list of mesh, num_skipped and num_fallback. See doc/hessian.md.

A field that is at most LINEAR has an exactly zero Hessian everywhere — the one mesh-shape-independent guarantee. For a genuinely quadratic field the composition is exact on a structured/symmetric mesh away from its own boundary and a good, standard, but genuinely approximate curvature estimate on an irregular mesh. Input must have exactly one component — a vector field's Hessian is a separate quantity per component.

A curvature-driven refinement indicator needs no new function: norm(...) in mio_data_calc() on the 9-component output is exactly its Frobenius norm, ready for mio_refine()'s where selector.

v10.8.0 additions ​

  • mio_data_integrate(mesh, names = NULL) — cell-measure-weighted total/mean of one or more cell_data arrays, mio_gradient()'s integration counterpart (mio_gradient differentiates a field, this integrates one), returning a list of per-array summaries — each with name, num_components, domain (num_cells, num_skipped, and a num_components x 4 total/mean/domain_measure/num_nan components matrix) and regions (a list of the same shape, one per named Cell region present). See doc/field_integration.md.

Every sum is weighted by |measure(cell)|; a cell whose measure is not computable, or a component whose value is non-finite, is excluded from that component's numerator and denominator, never given a fallback weight of 1. Regions are not a partition: a cell in two regions contributes fully to both. A point_data-only name fails, naming mio_data_point_to_cell() as the fix.

v10.5.0 additions ​

  • mio_undo_green(coarse, fine) — green-element undo: restores fine's transitional (closure-only) cells back to their original parent, read verbatim from coarse — a lookup, not a reconstruction, since mio_refine() never renumbers or prunes points, so a green parent's exact connectivity and cell_data are already sitting, byte-for-byte, in coarse at the row fine's refine:parent_id names. Returns a list of mesh, num_groups_undone and num_cells_removed. See doc/undo_green.md.

A two-mesh operation, like mio_interpolate(): coarse is the mesh a prior mio_refine(coarse, ..., record_hierarchy = TRUE, record_levels = TRUE) call was run on, fine is that call's output — both flags are required, record_hierarchy alone does not imply record_levels. The six reserved refine:* arrays are always dropped from the output; only a single-pass (levels = 1) hierarchy is supported, a deeper multi-level hierarchy being refused by name. Unlike mio_subdivide()/mio_agglomerate(), this operation has no winding repair or discrete sign branch anywhere in it — pure array bookkeeping, which on the Python side means a full numpy reference implementation rather than C++-core-only (this binding always calls the installed C library either way).

  • mio_conservative_interpolate(source, target, arrays = NULL, default_value = 0, on_conflict = "error") — mass-preserving field transfer, mio_interpolate()'s sibling: conserves sum(target value * target measure) == sum(source value * source measure) over the region the two meshes share, a property mio_interpolate()'s pointwise sampling does not have. Both meshes are simplexified first, accepting ragged/polyhedron blocks for free. Unlike mio_interpolate(), a NULL arrays means every source point_data and cell_data array. Output arrays are always Float64. Like mio_undo_green(), a two-mesh operation, and like mio_subdivide()/mio_agglomerate() this is C++-core only — the 3D clip kernel's discrete branches could disagree with a second implementation near a degenerate overlap, so there is no pure-R fallback either; the installed C library is always called. See doc/conservative_interpolate.md.

v10.4.0 additions ​

  • mio_agglomerate(mesh, target_group_size = 8) — polyhedral coarsening, the many-to-one counterpart to mio_subdivide(): greedy seed-and-grow over the mesh's shared-face dual, absorbing face-adjacent neighbours by accumulated shared-face area until each group reaches target_group_size members, then emitting one polyhedron per group whose faces are exactly its external boundary — conserving volume exactly, since internal faces are simply dropped rather than re-triangulated. Returns a list of mesh and cell_map. See doc/agglomerate.md.

Unlike every other opaque-result operation here, cell_map is a single flat, not per-block, 1-based vector — an agglomerated cell's output index is a function of which group it joined, not which input block it came from — so there is no subdivide_cell_maps()-style per-block helper here, only the same single-array shift mio_split()'s node map already uses. Like mio_subdivide(), there is no point_map: points are never pruned or renumbered, so mio_clean(mesh, remove_orphans = TRUE) is the documented follow-up for a minimal point set. A non-manifold input (a face shared by three or more cells) raises an R error naming the face rather than guessing.

v10.3.0 additions ​

  • mio_subdivide(mesh, record_parent_ids = FALSE) — polyhedral refinement: one polyhedral child per face of every eligible 3D cell, connected to a new interior point, returning a list of mesh and cell_maps. Needs no per-type template table — tabulated types (reduced to corners for a quadratic variant) and existing polyhedron blocks are handled uniformly — and is automatically conforming, unlike mio_refine(). See doc/subdivide.md.

Unlike mio_convert_cells(), there is no point_map: subdivide never prunes or renumbers an original point. cell_maps[[b]] is 1-based input cell → the index of its first child (one per face) in the corresponding output block, the same shape mio_convert_cells() already uses for its own one-to-many splits.

v10.2.0 additions ​

  • mio_estimate_error(mesh, array, method = "zz", marking = "none", marking_value = 0.0, output = "", marked = "", overwrite = FALSE) — the Zienkiewicz-Zhu recovery-based error indicator of a point-data field, plus optional marking, returning a list of mesh, global_error, num_skipped and num_marked. A composition of mio_gradient with the point↔cell averaging round trip, not a new kernel. See doc/error.md.

error:zz is always attached; error:marked too when marking is not "none", so mio_refine's own predicate needs no change to consume it. Cells that cannot be evaluated read NaN in error:zz and 0 (never NaN) in error:marked, counted in num_skipped and excluded from global_error/num_marked. The three counters come back as double, like every other 64-bit integer in this binding (see 64-bit integers).

v9.11.0 additions ​

  • The settings pipeline: mio_pipeline_run_file(settings_path) and mio_pipeline_run_json(json_text) run a whole settings.json (read → operation chain → write; PascalCase ops/keys) through the C++ engine, and mio_pipeline_has_json() reports whether the loaded library carries the JSON parser — a build without it signals an R error naming -DMESHIOPLUSPLUS_WITH_JSON=ON rather than missing a symbol. The flat ABI carries JSON text only; the structured run report is a recorded follow-up.

v9.10.0 additions ​

  • mio_gradient(mesh, array, op = "gradient", method = "green-gauss", location = "cell", output = "", component = -1L, overwrite = FALSE) — the gradient, divergence or curl of a point-data field, returning a list of mesh, num_skipped and num_fallback. See doc/gradient.md.

The two counters come back as double, like every other 64-bit integer in this binding (see 64-bit integers); they are exact well past any plausible cell count. component is negative for every component here — deliberately the opposite of mio_isosurface(), where negative means the row magnitude.

v9.1.0 additions ​

  • mio_read(..., lenient = TRUE) — see doc/selective_read.md.
  • mio_read(..., piece = k) — keep one piece of a partitioned file (VTKHDF partitions or composite blocks): 0 is the first, negative counts from the end, NULL (the default) merges every piece into one mesh with one cell region per piece; out of range is an error naming the piece count. See doc/selective_read.md.
  • mio_read(..., drop_ghosts = TRUE) — remove the ghost cells (halo) of a partitioned .pvtu, .pvtp or .pvd: every cell with a vtkGhostType bit set, and the points only they used. FALSE (the default) keeps them; every other reader ignores it. See doc/selective_read.md. The mio_read entry point in R/meshioplusplus.R takes the argument; the generated man/mio_read.Rd was already behind the roxygen source (no time_step/lenient); regenerate it with roxygen2::roxygenise().
  • XDMF series: mio_xdmf_series_flush(), mio_xdmf_series_finalized(), and mio_xdmf_series(..., mode = "append", auto_flush = FALSE).

As elsewhere in this binding, remember to release a series before its tempdir is removed: a write failure during the implicit finalize in a GC finalizer cannot be reported.

Format side channels ​

Gmsh mio_read_with_info returns a format-info handle; mio_gmsh_info(info) copies bounding_entities (signed tags per 4.1 cell block) and periodic links with entity (dimension, slave entity tag, master entity tag), affine (0 or 16 coefficients), and node_pairs ((2, N), 1-based point rows). Entity tags stay file ids; ordering and duplicates survive. All arrays use R double, like the other binding getters. mio_write_with_info(mesh, info, path, format = "gmsh") or "gmsh22" restores the records; mio_format_info_release(info) frees the handle, not the copies. Operations do not remap the channel; rebuild it after point/topology edits.

MDPA's mio_mdpa_info(info)$submodelparts entries contain name, data, tables, geometry_ids and constraint_ids. Membership values retain raw file ids, ordering and duplicates without R's point-index shift (represented as double, as other file ids are); mesh operations do not remap them. mio_write_with_info restores nested membership-only parts; constraints stay opaque in raw_blocks.

MFEM NURBS meshes (v16.40.0): mio_read_with_info(path, format = "mfem") returns the patches an MFEM NURBS mesh was sampled from, and mio_nurbs_info(info) copies them into a list: patches (degrees, knots, control_points as a (space_dimension, points) matrix, weights, fields), field_names, topology (NULL when the source states none) and the cell map cell_patch, cell_span and boundary_cell_patch. Vertices, patches and knot spans are 1-based; attributes and knot-vector ids stay as the file spelled them. mio_write_with_info(mesh, info, path, format = "mfem") writes the patches back (MFEM).

Kratos CAD JSON (and, with OpenCASCADE, STEP and IGES): mio_read_with_info(path, format = "cad_json") returns the B-rep a mesh was sampled from, and mio_brep_info(info) copies it into a list: patches (degrees, the full knots, control_points as a (3, points) matrix, weights), faces (id, name, 1-based patch, swapped_normal, trimmed, rational, reduced_knots, control_point_ids, loops of type, trims and extra, embedded_edges, surface_extra, extra; each trim trim_index, direction, curve, extra), edges (id, name, curve or NULL, topology, extra), vertices, breps (1-based faces, edges, vertices) and the file's extra. A curve is degree, knots, reduced_knots, rational, control_points, weights, ids, active_range, extra. Ids are -1 where the file names the entity instead; the copy outlives the handle, and mio_write_with_info() writes the B-rep back as CAD JSON.

mio_read_with_info(path, format = NULL, lenient = FALSE) returns list(mesh = , info = ), info a mio_format_info external pointer for a format with a side channel (MDPA: tables, geometries, Mesh blocks, sub-model-part data, text ModelPartData, raw blocks such as Constraints) and NULL otherwise. mio_write_with_info(mesh, info, path) puts it back, mio_format_info_release(info) frees it, and mio_mdpa_info(info) copies the MDPA side channel into a list (tables, geometries with 1-based connectivity, mesh_blocks, submodelparts, raw_blocks, …); ids arrive as double. See MDPA.

Sequences (transient / multi-file datasets) ​

mio_sequence() wraps the C API's ordered plan over a set of files (or the steps inside one multi-step file). Ordering is natural-numeric, so out_9.vtu precedes out_10.vtu; nothing is read until mio_sequence_read(), which hands back a mesh independent of the sequence — the sequence caches nothing, so a 500-step dataset is traversable without materialising it.

r
seq <- mio_sequence("out_*.vtu")            # or mio_sequence_list(c("a.vtu", ...))
for (i in seq_len(mio_sequence_count(seq))) {
  mesh <- mio_sequence_read(seq, i)         # one mesh alive at a time
  cat(mio_sequence_time(seq, i), mio_sequence_time_source(seq, i), "\n")
}
mio_sequence_to_timeseries(seq, "series.xdmf")            # fan-in
mio_sequence_resample(seq, "u_{index}.vtu", seq(0, 2, by = 0.1))  # onto new times
mio_sequence_free(seq)

# ascii=TRUE selects XDMF's "XML" data format (no HDF5 needed) -- the option
# this package's own HDF5-off notebook environment needs; see below.

mio_timeseries_to_sequence("series.xdmf", "step_{step}.vtu")   # fan-out
mio_sequence_pipeline_run_file("transient.json")               # per-step chain

Indices are 1-based, like every other R accessor. The external pointer has its own tag, so a mio_mesh, a mio_xdmf_series and a mio_sequence can never be passed for one another; a released handle is an R error, never a dereference.

mio_sequence_resample(seq, out_path, times, method = "linear", clamp = FALSE, blend_points = FALSE, out_format = NULL) resamples onto new times ("nearest" and "previous" too) holding at most two source meshes, and mio_blend_steps(a, b, w, blend_points = FALSE) blends two steps of one topology. mio_agglomerate() takes merge_coplanar_faces, coplanar_angle and min_sphericity and reports num_faces_merged and num_rejected (see agglomerate). See Resampling onto new times.

See sequences for the ordering rule, the time-value precedence and the streaming guarantee.

  • mio_grid(), mio_voxelize(), mio_sample_distance(), mio_distance_to_surface() and mio_surface_watertight_check() (v9.24.0) — regular grids and signed distance. The mio_ prefix keeps mio_grid() clear of base R's own grid package. Counters come back as double, as everywhere else here. See doc/voxelize.md and doc/sdf.md.
  • mio_rectangle(), mio_box(), mio_disk(), mio_uv_sphere(), mio_icosa_sphere(), mio_cylinder(), the planar domains mio_reference_triangle(), mio_ngon(), mio_annulus(), mio_lshape(), mio_plate_with_hole() and mio_honeycomb(), and mio_reference_cells() — parametric generators that take no input mesh, like mio_grid(); arguments mirror the Python functions, cell_type and split are strings, and a bad request is an R error naming the shape. See parametric primitives.
  • mio_octree() — an octree (quadtree for a planar line skin) around a mesh, fine only where the skin is (root_cells, min_level, max_level, balance, fill, transition, snap, surface_level, curvature_threshold, feature_angle, size_array, plane, ...); returns a list with mesh, dimension, num_leaves, num_hanging, finest_level, origin and root_size. See doc/octree.md.
  • mio_octree_mesh() — mesh the inside of a skin on a balanced octree: graded tetrahedra by default, output = "castellated" for the hexahedral leaves (output, min_size, max_size, max_level, bounds, curvature_angle, feature_angle, gap_cells, size_array, surface_region, skin_grading, conformity, fill, keep_outside, attach_distance, attach_quality, max_cells, max_tets, ...). Every argument left NULL keeps the mio_octree_mesh_opts_init() default (the Python defaults), and an unknown output, conformity, skin_grading or fill is an R error. Returns a named list with mesh, every mio_octree_mesh_report field (output as its name, counters as double) and warnings, a character vector with one entry per warning. See doc/octree_mesh.md.
  • mio_compute_sdf() (v9.25.0) — the grid and the field in one call, returning a named list (mesh, dims, origin, spacing, max_depth, num_banded, quality). structure = "octree" refines only near the surface and sizes itself from root_resolution/max_depth, so passing resolution or cell_size with it is an error; its output is 1-irregular.
  • mio_crop_predicate(mesh, array, compare, value) (v9.25.0) — keep the cells whose scalar cell_data value satisfies a comparison. There is deliberately no mode: a per-cell value has nothing for an all/any rule to reduce. See doc/crop.md.

Released under the MIT License.