Julia
Exodus series and pipeline Version 2
ExodusSeries(path) (or its do-block form) owns the fixed-grid Exodus writer. Call write_points_cells!(series, mesh) once and write_data!(series, time, mesh) for each step; flush!, finalize!, finalized, num_steps and idempotent close follow the XDMF lifecycle. netCDF is required; named MeshioErrors report missing support. Geometry, sets and attributes are static; field schema is fixed by the first step. See Exodus.
Existing run_pipeline_* and report routines accept Version 2 without new parser dependencies: auxiliary path Inputs and terminal Output.Pattern with {key}/{part}. Transient sequence schemas remain Version 1.
Pipeline reports
run_pipeline_file_report(path), run_pipeline_json_report(text) and run_sequence_file_report / run_sequence_json_report return structured report JSON (steps and warnings) without adding package dependencies. The native owner is always released in finally; existing status-only APIs are unchanged. See pipeline reports.
MED named meshes
med_mesh_names(path) enumerates file link order; read_med_named(path, name; options=ReadOptions()) returns an owning Mesh (close it normally); write_med_multi(path, meshes, names; version="4.1.0") borrows input meshes only for the call. Names must be unique/nonempty, without / or @. HDF5 is required; ordinary nodal/element profiles expand with NaN fill. See MED.
Parameterized writes support mio.write(mesh, path; encoding="binary", codec="lzf") for PCD compressed binary (no optional dependency; ASCII/non-PCD combinations fail), and mio.write(mesh, "run.frd"; encoding="binary") writes CalculiX's binary result layout. write_gltf(mesh, path; color_by="temperature", cmap="turbo", up_axis="z", scale=0.001) exposes all native glTF options; component is 1-based or nothing for magnitude, and unset vmin/vmax selects automatic bounds. Native reads accept XDMF2/3 and absolute DataItem references and carry Netgen names/periodic arrays; Netgen periodic arrays use field_data and keep file node ids 1-based, while .vol.gz requires native zlib.
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 a Julia package, MeshioPlusPlus, layered on the C API via ccall — the same way the Fortran module is, and aimed at the same HPC audience:
import MeshioPlusPlus as mio
using MeshioPlusPlus
m = mio.read("bracket.msh")
println(m) # Mesh(9231 points, 42145 cells in 3 blocks)
surf = extract_surface(m)
mio.write(surf, "bracket_surface.vtu")This binding is not MIT
bindings/julia/ is released under the GNU General Public License, version 3 (GPL-3.0), not the MIT licence covering the rest of meshio++. GPL-3.0 is a copyleft license, not a permission-required one: anyone — including a company — may use, modify or sell it commercially with no permission needed, but distributing it or a modified version of it must be under GPL-3.0 too, with source available. Purely private/internal use that is never distributed carries no obligation.
The C API and C++ core this binding calls are unaffected and stay MIT. Calling that stable, non-GPL C ABI via ccall/dlopen at runtime is the standard "linking exception" case — it does not require the C library to also be GPL. See bindings/julia/LICENSE.
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 read_with_info / write_with_info; output remains 4.1 (gmsh) or 2.2 (gmsh22). Info-less reads refuse $Periodic rather than lose it. See Gmsh.
Building and installing
The package binds the installed C library; no C++ is compiled by it.
./build/configure.sh --c-api --build
cmake --install build/cpp-release --prefix /opt/meshioplusplusThen either put /opt/meshioplusplus/lib on the loader path, or point the package straight at the library:
ENV["MESHIOPLUSPLUS_LIB"] = "/opt/meshioplusplus/lib/libmeshioplusplus.so"
using Pkg
Pkg.develop(path="bindings/julia/MeshioPlusPlus")MESHIOPLUSPLUS_LIB is checked first, then the standard loader path (Libdl.find_library). Failing both raises an error naming these two build commands.
A build with HDF5 can fail to load from Julia on Debian/Ubuntu
If the C API was built with -DMESHIOPLUSPLUS_WITH_HDF5=ON, loading it from a Julia process can fail with libcurl.so.4: version 'CURL_OPENSSL_4' not found. This is a real Debian/Ubuntu + Julia interaction, not a meshio++ bug: libhdf5-dev there pulls in libhdf5_openmpi, which links the system libcurl, while Julia bundles its own libcurl — and depending on the process's library-resolution order (an IJulia kernel subprocess is more likely to hit it than an interactive julia invocation), the two can conflict. Build with -DMESHIOPLUSPLUS_WITH_HDF5=OFF -DMESHIOPLUSPLUS_WITH_NETCDF=OFF if this binding is all you need from the library, as the julia CI job does.
No registration yet, and no JLL
GPL-3.0 is OSI-approved, so the package is eligible for Julia's General registry — but registering it is a separate follow-up step, not done yet, so Pkg.add("MeshioPlusPlus") will not work until then; install by path or by URL in the meantime.
There is also deliberately no JLL: shipping a binary artifact through BinaryBuilder/Yggdrasil is a real distribution step and belongs in a follow-up rather than being faked here.
Array layout and 1-based indexing
The C core stores points row-major as (num_points, dim) and connectivity as (num_cells, nodes_per_cell). Because Julia is column-major, the same memory is naturally
points(m) # (dim, num_points)
connectivity(m, 1) # (nodes_per_cell, num_cells)
point_data(m, "displacement") # (components, num_points)so nothing is ever transposed — exactly the reasoning the Fortran module documents. Node indices are 1-based here, and the ±1 shift happens inside the copying accessors, where a copy is made anyway:
| accessor | copies? | node indices |
|---|---|---|
points(m) | yes | n/a — coordinates carry no indices |
points_ptr(m) | no (zero-copy borrow) | n/a |
connectivity(m, i) | yes | 1-based |
connectivity_ptr(m, i) | no | 0-based — the ABI's own |
A borrow cannot be shifted without copying it, which is the whole point of a borrow — hence the two names. point_data_ptr, cell_data_ptr and field_data_ptr follow the same pattern.
Index maps and permutations (refine, convert_cells, subdivide, agglomerate, decimate, partition, reorder) are copies, so they are 1-based too, and the C API's -1 "pruned / absent" sentinel becomes 0 — never a valid 1-based index. That is verbatim the Fortran rule; the bindings agree deliberately.
partition_labels is the exception: those are part ids, not indices, so they stay in 0:nparts-1.
The borrow window
points_ptr and friends return a MeshBorrow, a real zero-copy view of the buffer inside the C++ core (unsafe_wrap). The C API's rule is that such a pointer stays valid until the next mutating mio_mesh_* call on that mesh, or until it is freed; read-only accessors never invalidate it.
MeshBorrow enforces exactly that rather than trusting the caller:
b = points_ptr(m)
b[1, 2] # fine
add_point_data!(m, "T", temperatures) # a MUTATING call ends the window
b[1, 2] # BorrowError -- not a stale readIt also holds a reference to the owning Mesh, so the mesh cannot be garbage-collected while a view of it is alive. parent(b) returns the raw Array for hot loops; that escape hatch is unchecked and valid only inside the window.
Memory management
A Mesh releases its handle through a finalizer — the one real difference from the Fortran module, where meshes are freed explicitly with call m%free(). close(m) releases one deterministically and is idempotent.
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, riding the raw C-side numbering unshifted like partition_labels' part ids — and forces refine:entity (the multigrid prolongation stencil) to be attached even when the closure leaves no hanging node. See refine.
Operations producing an opaque C result (split, partition, reorder, refine, decimate, convert_cells, subdivide, agglomerate) always transfer ownership of the mesh out of that result rather than handing back a borrow into it, so a piece stays valid after the result is gone:
for (key, piece) in mio.split(m; by="type")
mio.write(piece, "part_$key.vtu") # still valid; the result is long gone
endNames that would shadow Base
read, write, convert, merge, split and diff are not exported, because they collide with Base. Call them qualified:
import MeshioPlusPlus as mio
m = mio.read("bracket.msh")Everything else — the accessors, the setters, regions, and the remaining operations — is exported. close and isopen are proper Base method extensions.
Error handling
Every failure raises a MeshioError carrying the C API's own thread-local message; a status code never reaches the caller.
julia> mio.read("/nope.vtu")
ERROR: MeshioError (read error): meshio++: cannot open file '/nope.vtu'Using a borrow outside its window raises BorrowError instead.
Named regions
add_region!(m, "inlet", :point, [1, 3, 5])
add_region!(m, "solid", :cell, [1, 2]; dim=3, tag=17)
add_region!(m, "wall", :side, Int64[1 2; 0 2]) # (cell, facet) pairs
for r in regions(m)
println(r.name, " ", r.kind, " ", size(r.entries, 2), " entries")
endEntries are 1-based, with one exception documented in 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 — the same rule Fortran's partition_labels follows.
Selective reads and time steps
ReadOptions narrows what a read materializes, and since v8.6.0 also picks which time step of a multi-step file to decode:
using MeshioPlusPlus
m = MeshioPlusPlus.read("big.vtu"; options = ReadOptions(points_only = true))
# 0 (the default) is the first step; negative counts from the end.
last = MeshioPlusPlus.read("run.exo"; options = ReadOptions(time_step = -1))
meta = read_metadata("run.exo")
meta.time_values # [0.0, 0.5, 1.0] -- how many steps `time_step` may nameFormat 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.
Out of range is an error naming the available count, never a silent clamp. meta.time_values is empty for a format with no time concept, so length(...) is always safe. Honoured by exodus; see Selective reads.
Transient (time-series) XDMF writing
XdmfSeries is the write half of the above, and the one writer write cannot express: a series is a stateful multi-call object, so it gets its own handle rather than a keyword. The grid goes out once and each solve appends a cheap step. See XDMF time series.
using MeshioPlusPlus
s = XdmfSeries("simulation.xdmf") # "HDF" by default
write_points_cells!(s, mesh) # the static grid, once
for k in 0:nsteps-1
solve!(mesh)
write_data!(s, k * dt, mesh) # point_data/cell_data only
end
num_steps(s) # 10
finalize!(s) # close(s) would do this too
close(s)The function form closes the series afterwards even if the body throws:
XdmfSeries("simulation.xdmf"; data_format = "XML") do s
write_points_cells!(s, mesh)
write_data!(s, 0.0, mesh)
enddata_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, throws a MeshioError from the constructor.
Two things worth knowing before reading the result back:
- the
.xdmflight data is buffered until the series is finalized, so the file is only readable afterfinalize!(s)(orclose(s), which finalizes first); closeswallows a write failure during that implicit finalize — callfinalize!explicitly to see one.
Like Mesh, the handle is released by a GC finalizer and close is the deterministic, idempotent form. The name is finalize! rather than finalize because Base.finalize runs an object's GC finalizer and means something quite different. Reading a finished series back is the ordinary MeshioPlusPlus.read(path; options = ReadOptions(time_step = k)).
Sets ↔ integer data
sets_to_data(mesh, :point; data_name=nothing, join_char="-", order=String[]) and data_to_sets(mesh, :cell, "material") operate on region-backed memberships and return a new mesh. Labels remain zero-based (uncovered rows are -1), even though region accessors are 1-based. See sets/data conversions.
Documented gaps
These are gaps in the C ABI, shared with the Fortran and R bindings; the Julia package invents no workaround for any of them:
Four Python-only formats.
pmsh,zarr,caeandusd(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.the
frozenpin mask ofsmoothanddecimate;per-cell-type counts in
stats— usecell_block_typeswithcell_block_info;ragged block connectivity— closed in v9.15.0:polygon_block/polyhedron_blockread them as nested 1-based vectors andadd_polygon_block!/add_polyhedron_block!build them.connectivitystill throws, since a ragged block has no matrix. See Polyhedra and ragged cells;the combined
data_manage—data_drop/data_keep/data_renamecompose to the same effect;Exodus provenance strings (
qa_records/info_records): they ride theExodusInfoside channel, which likeMedInfodoes not cross the flat ABI, somesh.infohas no counterpart here. Geometry, data, regions and time steps are unaffected.
One further limitation is a format one rather than an ABI one, and easy to trip over: gmsh does not currently round-trip named regions. The writer emits the $PhysicalNames entry but does not attach the physical tag to an entity in $Elements, so a reader finds nothing to rebuild the group from. This is pre-existing meshio++ behaviour, reproducible from Python; abaqus round-trips regions correctly.
Tests
MESHIOPLUSPLUS_LIB=/opt/meshioplusplus/lib/libmeshioplusplus.so \
julia --project=bindings/julia/MeshioPlusPlus -e 'using Pkg; Pkg.test()'The suite uses the same deliberately non-square fixture as tests/fortran/test_fortran_api.f90 — 5 points × 3 dims, 2 tetrahedra × 4 nodes, 3-component vector data — so a transposed mapping or a missed shift cannot cancel out and pass anyway. It pins the column-major identity, the 1-based/0-based accessor pair, the borrow window, regions, and every operation.
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 keyword argument of the same name as the C mio_render_opts field.
frame = render(mesh; width=800, height=600, supersample=2,
color_by="temperature", colorbar=true, clip=(2, 98), isolines=8)
frame.rgba # Array{UInt8,3} (4, width, height); frame.rgba[:, x, y] is a pixel, row 1 on top
frame.cell_ids # (width, height): the input cell at each pixel, -1 for none
frame.range # (vmin, vmax), or nothing when no field is mapped
frame.notes # the colour range, ticks and keys
print(render_text(mesh; cols=80, rows=24, encoding="braille", color_by="temperature"))
png = render_png(mesh; width=800, height=600) # Vector{UInt8}
write_snapshot("part.png", mesh) # .png .txt .ansi .html .castCut-aways (v16.36.0). cutaway=[px, py, pz, nx, ny, nz] (a point and the normal of the side kept), or a collection of up to two such planes, with cutaway_tint=rgba(...), clips the geometry away and tints the back faces now in view: render(mesh; view="-x", cutaway=[0.5, 0, 0, 1, 0, 0]). A wrong length is an ErrorException; a zero normal a MeshioError.
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=rgba(...): render(mesh; streamlines="velocity", stream_seeds=60). An unknown array or a seed count outside 1 to 10000 is a MeshioError.
render copies the frame out of the library, so a Frame owns its arrays and nothing needs closing. Colours are 0xRRGGBBAA integers or (r, g, b[, a]) tuples (rgba(r, g, b, a) builds the integer); shading, edges, scale and diagnostic take their names ("smooth", "feature", "log", "orientation", ...). render_text(...; format="html") returns a self-contained page, which a notebook shows with display("text/html", ...). An unknown option or enum name is an ErrorException listing the choices; a library failure (an unknown array, a bad range) is a MeshioError. detect_color_depth(; no_color, color_term, term) returns "truecolor", "256", "16" or "mono".
v16.25.0 additions
agglomerate(mesh; target_group_size=8, merge_coplanar_faces=false, coplanar_angle=1.0, min_sphericity=0.0) -> (; mesh, cell_map, num_faces_merged, num_rejected)— the coplanar-face merge and sphericity gate of agglomerate.resample(seq, out_path, times; method=:linear, clamp=false, blend_points=false, format="")— resample aSequenceonto new times (:linear,:nearestor:previous), at most two source meshes live; andblend_steps(a, b, w; blend_points=false) -> Mesh. See Resampling onto new times. Both are exported.
v15.4.0 additions
compute_normals(mesh; point_normals=true, cell_normals=false, weight=:angle, split_angle=nothing, record_parent_ids=false, region="") -> (; mesh, quality, num_isolated, num_undefined, num_degenerate, num_split_points, num_added_points)— point and cell normals of a surface, written asnormals((n, 3)point data;(cells, 3)cell data withcell_normals).split_angle=nothinggives 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. Never reorients: checkquality.inconsistent_pairs. See normals.check_quality(mesh; require="", max_inverted=0, max_degenerate=0) -> (; passed, num_checks, num_failed, num_cells, num_inverted, num_degenerate, summary)— the quality gate.feature_edges(mesh; feature_angle=30.0, feature=true, boundary=true, non_manifold=true, inconsistent=true, region="") -> (; mesh, num_feature, num_boundary, num_non_manifold, num_inconsistent)— the sharp, open, non-manifold and inconsistently wound edges of a surface (or of a volume mesh's skin) as alinemesh withfeature:kind/feature:angle. See feature edges.tessellate(mesh; levels=2, curved=true, fields=true, record_stencil=false) -> (; mesh, 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, num_facets_with_bad_count)— isoparametric tessellation oftriangle6/quad8/quad9/tetra10/hexahedron27cells into triangles and tetrahedra, withtessellate:source_pointpoint data (0-based source point, or -1),tessellate:source_cell/tessellate:sub_indexcell data and, withrecord_stencil, the(27, num_points)tessellate:stencil/tessellate:weights; see tessellation.segment(mesh; feature_angle=30.0, normal_tolerance=0.0, curvature_tolerance=0.0, min_patch_cells=1, region="", prefix="patch", method="angle", tolerance=1e-3) -> (; mesh, num_patches, num_merged, num_feature_edges, num_boundary_edges, num_non_manifold_edges)— smooth patches bounded by the feature edges, assegment:patchcell data,segment:adjacencyfield data and one cell region per patch,method="primitive"ending a patch where its plane, cylinder, cone, sphere or torus does; see segment.fit_primitives(mesh; patch_data="segment:patch", tolerance=1e-3, relative=true, min_inlier_fraction=0.9, types=("plane", "sphere", "cylinder", "cone", "torus"), max_iterations=50) -> (; mesh, num_patches, num_plane, num_sphere, num_cylinder, num_cone, num_torus, num_unfitted)— a primitive fitted to each patch, withfit:*cell data andfit:patch_*field data; see fit primitives.refine_patches(mesh; tolerance=1e-3, relative=true, normal_weight=1.0, epsilon=1e-3, feature_angle=30.0, max_iterations=10, min_patch_cells=3, ransac_seed=0x6d657368696f2b2b, types=nothing, allow_split=true, allow_merge=true, prefix="patch") -> (; mesh, num_patches, num_iterations, num_splits, num_merges, num_absorbed, num_primitive, num_freeform, num_facets, tolerance, explained_area_fraction, initial_explained_area_fraction, hausdorff_max, hausdorff_rms, initial_energy, energy, stop)(v17.11.0) — refines thesegment:patchlabels by variational shape approximation, moving boundaries, splitting a freeform patch where seeded RANSAC finds a primitive inside it and merging neighbours one primitive explains, and returns the mesh with the patches, regions andfit:*data rewritten plusrefine:*field data.types=nothingallows every primitive,ransac_seedis aUInt64(exact over its whole range, unlike R's doubles) andstopis:unchanged,:small_gain,:iteration_capor:energy_rose; a mesh withoutsegment:patchor an option out of range throws aMeshioError. See refine patches.iga_refine(info; subdivisions=1, elevate=0, sample_subdivisions=0, trim_samples=16, edges=true, trim=true) -> (Mesh, FormatInfo),iga_sample(info; sample_subdivisions=0, …) -> (Mesh, FormatInfo)andiga_evaluate(info, patch, params; derivatives=0, trim_samples=16) -> (; points, fields, derivatives, normals, inside)(patch1-based,paramsa(d, N)matrix; absent parts arenothing) act on the NURBS model of aread_with_infohandle (MFEM NURBS, CAD JSON, STEP, IGES); the returnedFormatInfowrites back throughwrite_with_info. See isogeometric layer.mesh_cad_surface(info; target_size, max_chordal_deviation=0.0, min_points_per_edge=0, tolerance=0.0, record_face_ids=true, edges=true, match_unreferenced_trims=true, strict=false, max_points=50_000_000, min_angle=0.0, smooth_iterations=0, smooth_method="laplacian", order=1, grading=0.0, curvature_deviation=0.0, cells_across_gaps=0.0, pole_rings=0, face_sizes=nothing, edge_sizes=nothing, background=nothing, background_array="size", metric="mean", virtual_edges=false, reparam_condition_number=0.0, model_tolerances=false, virtual_faces=false, lop_corners=false, metric_background=nothing, metric_array="", metric_error=0.0) -> (; mesh, num_points, num_triangles, …, faces, warnings, angle_histogram, corners, virtual_edges, reparametrised, merged_faces, tolerance_uses, lopped_corners)triangulates the faces of acad_json,steporigeshandle into one conforming surface mesh;facesholds each face's 1-based index,statusandreason.min_anglerefines to a smallest angle on the surface andsmooth_iterationssmooths ("laplacian"or"odt"); the result addsnum_refinement_points,num_faces_below_bound,num_smoothed,min_input_angle_deg, the 18 ten-degree bins ofangle_histogramand thecornersunder 60 degrees and poles (1-basedfaceandpoint).order2 or 3 (v17.16.0) writes curvedtriangle6ortriangle10elements whose nodes lie on the geometry, andnum_curved_straightenedcounts those kept straight-sided.curvature_deviationandcells_across_gaps(v17.18.0) size the mesh by the model's curvature and gaps,gradingbounds the size's growth andpole_ringsgrades the poles.face_sizesandedge_sizes(v17.19.0; aDictor pairs of 1-based indices and sizes) and abackgroundmesh with its point databackground_arraygive sizes that join the same field,metric="varying"meshes each face in its surface's own metric andvirtual_edgesskips slivers of edges; each face addslimited_by_edges(1-based) and the resultvirtual_edges.reparam_condition_number(v17.20.0) meshes a face whose parameters stretch more than that in a flattening of its coarse mesh,virtual_facesmeshes a sliver face with its tangent neighbour,model_tolerancesaccepts the gaps the model declares andlop_cornerslops acute corners (Pav and Walkington 2005) in place of concentric shells; ametric_backgroundmesh whose 9-component point datametric_arrayis a field's Hessian, with the target interpolation errormetric_error, sizes the triangles by an anisotropic metric (roadmap 6.10.2); the result addsreparametrised(face,condition_number,condition_number_disc,taken,method,reason),merged_faces(sliver,host,width,edges,merged,reason) ,tolerance_uses(edge,declared,gap,where) andlopped_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.fit_cad(mesh; step_path="", tolerance=0.0, exact_edges=true, types=("plane", "sphere", "cylinder", "cone", "torus"), mesh_deflection=0.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=10, refine_epsilon=1e-3, normal_weight=1.0, allow_merge=true, allow_split=true, ransac_seed=0x6d657368696f2b2b, refine_tolerance=1e-3, relative_tolerance=true, freeform_general=false, freeform_tolerance=0.0, freeform_smoothing=1e-12, max_freeform_poles=40, max_freeform_iterations=30, max_freeform_splits=2, sew_tolerance=0.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.0, freeform_max_control_points=400, layout_target_patch_size=0.1, layout_max_valence_deviation=2, resegment_rounds=2, layout_snap_to_features=true, validate_by_surface_sampling=true) -> (; mesh, backend, valid, closed, tolerance, volume, skin_volume, volume_ratio, max_deviation, num_faces, num_plane, num_sphere, num_cylinder, num_cone, num_torus, num_bspline, num_faceted_faces, num_patches, num_demoted, num_exact_edges, num_approx_edges, num_solids, num_split_patches, num_fillet, num_helical, num_variable_fillet, num_blend, num_rounded_thread, num_vertex_blend, num_chamfer, num_conic_blend, num_tapered_thread, num_variable_pitch_thread, refined, refinement_kept, refine_iterations, num_refine_splits, num_refine_merges, initial_explained_area_fraction, explained_area_fraction, num_freeform_general, num_freeform_refinements, avg_freeform_poles, num_extended_edges, num_snapped_junctions, hausdorff_max, hausdorff_rms, brep_checked, freeform_mode, facet_area_fraction, 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, num_face_records)— the fitted patches as a CAD solid with analytic faces (and, for the patches no primitive explains, rolling-ball fillet and blend faces withfillets, helical thread faces withthreadsand, withfreeform, B-spline faces, a patch folding over every flattening split into pieces withfreeform_split), written as STEP tostep_pathwhen given;backend"occt"needs a library built with OpenCASCADE, whichhas_occt()reports, and throws aMeshioErrornaming-DMESHIOPLUSPLUS_WITH_OCCT=ONotherwise,"native"writes the fitted primitives as analytic STEP faces with no CAD kernel, and"auto"picks OpenCASCADE when present;backendin the result names the one that ran. Since v17.11.0,refine_segmentation=truefirst refines the patches asrefine_patchesdoes (throughmio_fit_cad2), withrefine_tolerance(relative whilerelative_tolerance),normal_weight,refine_epsilon,max_refine_iterations,allow_split,allow_mergeandransac_seed;refinedsays whether it ran,refinement_keptwhether the solid was built from the refined patches (the OpenCASCADE backend also builds it from the patches as given and keeps the better), and the refinement's counts and explained area shares are zero when it did not.freeform_general=truetries 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;num_freeform_general,num_freeform_refinementsandavg_freeform_polesreport it), and, OpenCASCADE only,sew_tolerance(0: the tolerance) sets the sewing tolerance,extend_surfacesextends freeform surfaces past their patches (num_extended_edges),snap_junctionsplaces the corners where analytic faces meet by least squares (num_snapped_junctions) andface_deviationmeasures each face against the skin (hausdorff_maxandhausdorff_rms,NaNwhen not measured, and thefit_cad:face_hausdorff/fit_cad:face_rmsfield data);closedsays whether every shell is closed. Since v17.12.0 the freeform modes go throughmio_fit_cad3:freeform_mode("legacy","adaptive","network"or"auto"),freeform_parameterization("auto","projection","lscm","harmonic"),extraordinary_vertex("g0","g1","g1x","g1xx") andlocal_refine("knots","thb") as strings, an unknown one anArgumentError, with the layout and budget keywords above; the result always carriesbrep_checked,freeform_mode,facet_area_fraction(the share of the area left on facet faces), the network and escalation counts,max_g1_angle_deg, the freeform distances andnum_face_records, and outside"legacy"the mesh carries the face records asfit_cad:face_tablefield data (one row per patch tried as a surface). See fit CAD.hausdorff_distance(a, b; face_samples=0, region_a="", region_b="") -> (; distance, a_to_b, b_to_a, mean_a_to_b, rms_a_to_b, mean_b_to_a, rms_b_to_a, num_samples_a, num_samples_b, worst_point_a, worst_point_b). See Hausdorff distance.edit_regions(mesh, op, inputs; output="", kind=nothing, dim=nothing, tag=nothing, keep_inputs=true) -> Mesh(opone of:union,:intersection,:difference,:rename,:retag,:delete) andremove_region!(mesh, index)(1-based). See editing regions.region_adjacency(mesh; regions=nothing) -> Meshreturns conforming shared facets between Cell regions, falling back to multiple cell blocks when fewer than two Cell regions exist — see region adjacency.find_interface(mesh_a, region_a, region_b; mesh_b=nothing, mode=:conforming, master=:a, gap_tolerance=0, angle_tolerance=30, overlap_tolerance=0) -> (; mesh, report)returns the master facets and source Side entries;contact_pairs(slave, slave_points, master_cells; master_mesh=nothing, tolerance=0, require_complete=false)returns copied point/facet projections;split_interface(mesh, side; add_cohesive=false)duplicates the incident-cell fans. Point and global cell ids are 1-based; local facet/subfacet ordinals remain 0-based. See interfaces and contact.match_periodic_nodes(mesh, slave, master; translate=nothing, matrix=nothing, atol=1e-8, require_complete=true) -> (; slave, master, unmatched, num_fixed, max_residual)— 1-based node ids;matrixis 16 numbers, row-major. See periodic node pairs.
v10.9.0 additions
hessian(mesh, array; method=:green_gauss, location=:cell, output="", overwrite=false) -> (; mesh, num_skipped, num_fallback)— the Hessian (second derivative) of a scalar point-data field,gradient's companion one order further. A composition of TWOgradientcalls, not a new numerical kernel: the field is differentiated once (point location), then that(n, 3)gradient is differentiated again with the default:gradientoperator, producing(n, 9)— the flattened row-major 3x3 Hessian,H[i,j]at indexi*3+j.methodis forwarded to BOTH internal passes. Seedoc/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 data_calc on the 9-component output is exactly its Frobenius norm, ready for refine's where selector.
hessian shadows nothing in Base, so unlike read/write/split it is exported.
v10.8.0 additions
data_integrate(mesh, names=String[])— cell-measure-weighted total/mean of one or morecell_dataarrays,gradient's integration counterpart (gradientdifferentiates a field, this integrates one), returning aVector{NamedTuple}withname,num_components,domain(num_cells,num_skipped,components— a vector of(total, mean, domain_measure, num_nan)NamedTuples) andregions(the same shape, one per named Cell region present). Seedoc/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 throws a MeshioError naming data_point_to_cell as the fix.
data_integrate shadows nothing in Base, so unlike read/write/split it is exported.
v10.5.0 additions
undo_green(coarse, fine)— green-element undo: restoresfine's transitional (closure-only) cells back to their original parent, read verbatim fromcoarse— a lookup, not a reconstruction, sincerefinenever renumbers or prunes points, so a green parent's exact connectivity and cell_data are already sitting, byte-for-byte, incoarseat the rowfine'srefine:parent_idnames. Returns(; mesh, num_groups_undone, num_cells_removed). Seedoc/undo_green.md.
A two-mesh operation, like interpolate: coarse is the mesh a prior 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 subdivide/agglomerate, this operation has no winding repair or discrete sign branch anywhere in it — it is pure array bookkeeping, which on the Python side means it has a full numpy reference implementation rather than being C++-core-only (this binding always calls the installed C library either way).
undo_green shadows nothing in Base, so unlike read/write/split it is exported.
conservative_interpolate(source, target; arrays=String[], default_value=0.0, on_conflict="error")— mass-preserving field transfer,interpolate's sibling: conservessum(target value * target measure) == sum(source value * source measure)over the region the two meshes share, a propertyinterpolate's pointwise sampling does not have. Both meshes are simplexified first, accepting ragged/polyhedron blocks for free. Unlikeinterpolate, an emptyarraysmeans every source point_data and cell_data array. Output arrays are always Float64. Likeundo_green, a two-mesh operation, and likesubdivide/agglomeratethis 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-Julia fallback either; the installed C library is always called. Seedoc/conservative_interpolate.md.
v10.4.0 additions
agglomerate(mesh; target_group_size=8)— polyhedral coarsening, the many-to-one counterpart tosubdivide: greedy seed-and-grow over the mesh's shared-face dual, absorbing face-adjacent neighbours by accumulated shared-face area until each group reachestarget_group_sizemembers, 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(; mesh, cell_map). Seedoc/agglomerate.md.
Unlike every other opaque-result operation here, cell_map is a single flat, not block-indexed, 1-based array: an agglomerated cell's output index is a function of which group it joined, not which input block it came from, so _result_map (the same single-array reader split's node map uses) reads it rather than _result_cell_maps. Like subdivide, there is no point_map — points are never pruned or renumbered, so 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 a MeshioError naming the face rather than guessing.
agglomerate shadows nothing in Base, so unlike read/write/split it is exported.
v10.3.0 additions
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(; mesh, cell_maps). Needs no per-type template table — tabulated types (reduced to corners for a quadratic variant) and existing polyhedron blocks are handled uniformly through the samedetail::cell_rings/orient_ringsmachinerygradientuses — and is automatically conforming, unlikerefine. Seedoc/subdivide.md.
Unlike 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 FirstChild shape convert_cells already uses for its own one-to-many splits.
subdivide shadows nothing in Base, so unlike read/write/split it is exported.
v10.2.0 additions
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(; mesh, global_error, num_skipped, num_marked). A composition ofgradientwith the point↔cell averaging round trip, not a new kernel. Seedoc/error.md.
error:zz is always attached; error:marked too when marking is not :none, so refine's own where selector needs no change to consume it — refine(mesh, where="error:marked > 0.5"). 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.
estimate_error shadows nothing in Base, so unlike read/write/split it is exported.
v9.11.0 additions
- The settings pipeline:
run_pipeline_file(settings_path)andrun_pipeline_json(json_text)run a wholesettings.json(read → operation chain → write; PascalCase ops/keys) through the C++ engine, andpipeline_has_json()reports whether the loaded library carries the JSON parser — a build without it raises aMeshioErrornaming-DMESHIOPLUSPLUS_WITH_JSON=ONrather than missing a symbol. The flat ABI carries JSON text only; the structured run report is a recorded follow-up. All three are exported (none shadowsBase).
v9.10.0 additions
gradient(mesh, array; operator=:gradient, method=:green_gauss, location=:cell, output="", component=-1, overwrite=false)— the gradient, divergence or curl of a point-data field, returning(; mesh, num_skipped, num_fallback). Seedoc/gradient.md.
Two spellings to note. method is given as an underscored symbol (:green_gauss, :least_squares) and translated to the C API's hyphenated name by the binding, because a Julia symbol cannot carry a hyphen. And component is negative for every component here — deliberately the opposite of isosurface, where negative means the row magnitude.
gradient shadows nothing in Base, so unlike read/write/split it is exported.
v9.1.0 additions
ReadOptions(; lenient=true)— seedoc/selective_read.md.ReadOptions(; piece=k)— keep one piece of a partitioned file (VTKHDF partitions or composite blocks):0is the first, negative counts from the end,nothing(the default) merges every piece into one mesh with one cell region per piece; out of range fails the read naming the piece count. Seedoc/selective_read.md.ReadOptions(; drop_ghosts=true)— remove the ghost cells (halo) of a partitioned.pvtu,.pvtpor.pvd: every cell with avtkGhostTypebit set, and the points only they used.false(the default) keeps them; every other reader ignores it. Seedoc/selective_read.md.- XDMF series:
flush!(s),finalized(s),XdmfSeries(path; mode=:append, auto_flush=false), andwrite_data!(s, t, Dict("u" => values))for writing a step from raw arrays with noMeshin between. Ann x kmatrix is transposed on the way out, since Julia is column-major and the C ABI expectskcomponents per entity row-major.
flush! is named with a bang for the same reason finalize! is: Base.flush means "flush this IO stream".
Format side channels
Gmsh read_with_info returns a FormatInfo; gmsh_info(info) copies bounding_entities (signed tags per 4.1 cell block) and periodic links (entity, affine, node_pairs). entity is (dimension, slave entity tag, master entity tag) in raw file ids, affine has 0 or 16 coefficients, and node_pairs is (2, N) with 1-based point rows, retaining ordering and duplicates. write_with_info(mesh, info, path; format="gmsh") or "gmsh22" restores the records. close(info) releases the handle, not the copies. Operations do not remap the channel; rebuild it after point/topology edits.
MDPA's mdpa_info(info).submodelparts entries contain name, data, tables, geometry_ids and constraint_ids. The last two lists retain original file ids, ordering and duplicates without Julia's point-index shift; mesh operations do not remap them. write_with_info restores nested membership-only parts, with constraints themselves kept opaque in raw_blocks.
MFEM NURBS meshes (v16.40.0): read_with_info(path; format="mfem") returns the patches an MFEM NURBS mesh was sampled from, and nurbs_info(info) copies them into a NamedTuple: patches (degrees, knots, control_points as (space_dimension, points), weights, fields as (components, points) matrices), field_names, topology (nothing when the source states none) and the cell map cell_patch, cell_span ((3, cells)) and boundary_cell_patch. Vertices, patches and knot spans are 1-based; attributes and knot-vector ids stay as the file spelled them. write_with_info(mesh, info, path; format="mfem") writes the patches back (MFEM).
Kratos CAD JSON (and, with OpenCASCADE, STEP and IGES): read_with_info(path; format="cad_json") returns the B-rep a mesh was sampled from, and brep_info(info) copies it into a NamedTuple: patches (degrees, the full knots, control_points as (3, points) with direction 1 fastest, weights), faces (id, name, patch, swapped_normal, trimmed, rational, reduced_knots, control_point_ids, loops of (type, trims, extra), embedded_edges, surface_extra, extra; a trim is (trim_index, direction, curve, extra)), edges (id, name, curve or nothing, topology, extra), vertices (id, name, topology, extra), breps and the file's extra. A curve is (degree, knots, reduced_knots, rational, control_points, weights, ids, active_range, extra). Indices (a face's patch, a brep's lists) are 1-based; ids (-1 where the file names the entity instead) and trim indices stay as the file spelled them. write_with_info writes the B-rep back as CAD JSON; the MIO_BREP_* section constants are exported for the raw accessors.
read_with_info(path; format="", options=nothing) returns (mesh, info), info a FormatInfo for a format that has a side channel (MDPA: tables, geometries, Mesh blocks, sub-model-part data, text ModelPartData, raw blocks such as Constraints) and nothing otherwise; write_with_info(mesh, info, path) puts it back and close(info) releases it. mdpa_info(info) copies the MDPA side channel into a NamedTuple (tables, geometries with 1-based (nodes, n) connectivity, mesh_blocks, submodelparts, raw_blocks, …); file ids stay as the file spelled them. See MDPA.
Sequences (transient / multi-file datasets)
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 read_step, and the sequence caches nothing, so a 500-step dataset is traversable without materialising it.
seq = Sequence("out_*.vtu") # or Sequence(["a.vtu", "b.vtu"])
for i in 1:length(seq)
mesh = read_step(seq, i) # OWNED; one mesh alive at a time
@show MeshioPlusPlus.time(seq, i), MeshioPlusPlus.time_source(seq, i)
end
to_timeseries(seq, "series.xdmf") # fan-in
to_timeseries(seq, "series2.xdmf"; ascii=true) # "XML" data format, no HDF5 needed
resample(seq, "u_{index}.vtu", 0:0.1:2) # onto new times, two meshes live
close(seq)
timeseries_to_sequence("series.xdmf", "step_{step}.vtu") # fan-out
run_sequence_file("transient.json") # per-step chainSequence is also iterable (for mesh in seq), and has a do-block form that closes the handle even if the body throws.
Not exported, because they shadow Base: path, step, time, time_source. Reach them as MeshioPlusPlus.time(seq, i) — the same rule the binding already applies to read/write/split.
See sequences for the ordering rule, the time-value precedence and the streaming guarantee.
grid(dims; origin, spacing),voxelize(m; resolution, fill, ...),sample_distance(surface, points),distance_to_surface(query, surface)andsurface_watertight_check(m)(v9.24.0) — regular grids and signed distance.gridshadows nothing inBase, so it is exported plainly. Seedoc/voxelize.mdanddoc/sdf.md.rectangle(; dims, size, origin, cell_type, split, order),box,disk,uv_sphere,icosa_sphere,cylinder, the planar domainsreference_triangle,ngon,annulus,lshape,plate_with_holeandhoneycomb, andreference_cells(types; pairs)— parametric generators that take no input mesh, likegrid; keyword arguments mirror the Python functions (sizeandcenterinstead of numpy arrays),cell_typeandsplitare strings, and an unknown one throwsArgumentError. None of the names shadowsBase. See parametric primitives.octree(m; root_cells, min_level, max_level, balance, fill, transition, snap, surface_level, curvature_threshold, feature_angle, size_array, plane, ...)— an octree (quadtree for a planarlineskin) aroundm, fine only where the skin is; returns(; mesh, dimension, num_leaves, num_hanging, finest_level, origin, root_size). Seedoc/octree.md.octree_mesh(m; 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, ...)— mesh the inside of a skin on a balanced octree: graded tetrahedra by default,output=:castellatedfor the hexahedral leaves. Every keyword left atnothingkeeps themio_octree_mesh_opts_initdefault (the Python defaults), and an unknownoutput,conformity,skin_gradingorfillthrowsArgumentError. Returns(; mesh, output, guarantee_applies, ..., num_inverted, num_warnings, warnings)with everymio_octree_mesh_reportfield andwarnings::Vector{String}; the struct mirrors are size-checked by_check_abi_layout. Seedoc/octree_mesh.md.compute_sdf(surface; structure, resolution, root_resolution, max_depth, ...)(v9.25.0) — the grid and the field in one call, returning(; mesh, dims, origin, spacing, max_depth, num_banded, quality).structure=:octreerefines only near the surface and sizes itself fromroot_resolution/max_depth, so passingresolutionorcell_sizewith it is an error; its output is 1-irregular.crop_predicate(m, array; compare, value, record_ids)(v9.25.0) — keep the cells whose scalarcell_datavalue satisfies a comparison, the same vocabularyrefine'swhere_opuses. There is deliberately nomode: a per-cell value has nothing for an all/any rule to reduce. Seedoc/crop.md.