Component I/O¶
Every registered component exposes its existing file-oriented entry point and an owning, in-memory interface:
void name(nlohmann::json options);
ComponentResult name(nlohmann::json options, const MeshMap& meshes);
MeshMap is std::map<std::string, MeshData>. ComponentResult is a pair of named
output meshes and a JSON report. The memory overload never resolves a selector as a filename,
never changes its input meshes, and does not emit mesh, report, log, or debug files. It does not
require application, input_dir, or output filenames. Python exposes both interfaces,
using plain dictionaries of NumPy arrays for mesh data.
#include <wmtk/io/mesh_io.hpp>
#include <wmtk/components/qslim/qslim.hpp>
wmtk::MeshMap meshes{{"surface", wmtk::io::read_mesh("input.obj")}};
auto [outputs, report] = wmtk::components::qslim::qslim(
{{"input", "surface"}, {"target_rel", 0.5}}, meshes);
wmtk::io::write_mesh("simplified.vtu", outputs.at("mesh"));
Python¶
Build and install in your active environment with python -m pip install -e ..
Every enabled component exposes name(options) for the existing file interface and
name(options, meshes) for the memory interface. The latter returns (meshes, report).
wildmeshing(options) remains compatible; wildmeshing(options, meshes) selects the memory
component from options["application"]. Direct component calls do not require application.
import numpy as np
import wildmeshing as wm
surface = {
"V": np.array([[0., 0., 0.], [1., 0., 0.], [0., 1., 0.]], dtype=np.float64),
"cells": {
"triangle": {"connectivity": np.array([[0, 1, 2]], dtype=np.int64)},
},
}
meshes, report = wm.qslim(
{"input": "surface", "target_rel": 1.0, "eps_rel": 0.0},
{"surface": surface},
)
positions = meshes["mesh"]["V"]
# Explicit file I/O is separate from the memory call:
wm.write_mesh("result.msh", meshes["mesh"])
loaded = wm.read_mesh("result.msh")
The dictionary mirrors the C++ data structure:
V: an(n, 3)NumPy array. Floating coordinates are promoted to Float64; integer coordinates must convert exactly. Returned positions always use Float64.cells: a dictionary keyed bypoint,edge,triangle,quad,tetrahedron,prism, orpyramid. Each block contains a two-dimensional integerconnectivityarray and an optionalattributesdictionary. Returned connectivity uses Int64.- Optional
vertex_attributesand cellattributesmap names to{"values": array, "labels": [column_names...]}. Labels may be omitted; a bare array is also accepted as an unlabeled input attribute. Attribute dtypes are Float64, Int64, or UInt8. One-dimensional input attributes become column matrices. Returned fields always containvaluesandlabels.
For example, overlapping region memberships are represented as:
mesh["cells"]["tetrahedron"]["attributes"] = {
"tags": {
"values": np.array([[1, 1], [1, 0]], dtype=np.uint8),
"labels": ["region_a", "region_b"],
},
}
Only selected dictionary entries are converted and validated. JSON selector arrays retain their order. Inputs may be read-only or strided arrays: the binding copies selected data into owning C++ arrays before computation. Outputs own their NumPy storage and can be passed directly into subsequent memory calls. Connectivity is never truncated from floating point, and Int64 attributes never pass through Float64. Computation releases the Python GIL. Component attribute contracts and the shared readers' and writers' supported formats are the same as in C++.
Run the Python memory tests with python -m unittest discover -s tests/python -p test_memory_io.py,
and those of tracking -- histories and wm.Tracking / wm.CurveTracking -- with
-p test_tracking.py; they are also included in the wheel tests.
MeshData¶
Vis an owning row-major Float64 matrix with three columns. Pad 2D coordinates with zero.cells[CellType]holds a row-major Int64 connectivity matrix and named attributes. Cell types are point (1 corner), edge (2), triangle (3), quad (4), tetrahedron (4), prism (6), and pyramid (5). Prism/pyramid ordering follows VTK.vertex_attributesand each cell block'sattributesmap names toMeshAttribute.valuesis a variant of row-major Float64, Int64, or UInt8 matrices. One row corresponds to a vertex or a row in that cell block's connectivity. Vectors are one-column matrices.MeshAttribute::labelsoptionally names columns.tagsuses a binary UInt8 matrix with one named column per physical/material group, allowing overlapping memberships. Group names are labels of this fixed field, rather than arbitrary extra attribute names.validate()checks geometry, connectivity, and attribute dimensions. Coordinates must be finite. Attributes may contain undefined numerical diagnostics such as NaN. A cell may repeat a vertex: components clean degenerate input cells exactly as their file readers do.
Only top-dimensional connectivity is needed to construct an algorithm's mesh. Explicit lower blocks identify the edges/faces carrying attributes; their row order is independent of internal mesh IDs. Do not use internal edge/face IDs as interchange indices. Exact positions, caches, rollback buffers, and mesh objects are not part of MeshData.
Input contracts¶
Selectors retain their component-specific JSON fields. A single selector is a dictionary key;
ordered selector arrays preserve input order and existing input_names / transform semantics.
Unselected dictionary entries are not validated. Missing keys and incompatible inputs throw
std::runtime_error before computation. Unknown attributes are rejected; this interface does
not transfer arbitrary user attributes through topology changes. Geometry-only inputs ("no
attributes" below) ignore the fields io::read_mesh attaches to plain geometry files: vertex
vid, and triangle uv and normal. A component may accept, and ignore, a field of its own
results as well, so that a result can be the next call's input as it is: isotropic_remeshing's
vertex_id, whose fixed vertices come from features alone. isotropic_remeshing also takes the
triangle fields its feature_fields names, as on the .msh route: the per-face ones it keeps as
labels through the remeshing and returns on mesh, each in the type it was given in, with its
labels (an Int64 one within 2^53 in magnitude, which Float64 holds exactly); uv and normal,
per corner (6 and 9 columns), it neither carries nor returns: their seams are curves, in edges.
So mesh goes back in with the same per-face fields named again.
| Component | Mesh selectors | Required data |
|---|---|---|
qslim |
input |
Triangle geometry, no attributes |
shortest_edge_collapse |
input |
Triangle geometry, no attributes |
isotropic_remeshing |
input; optional features |
Triangle geometry, no attributes but its own vertex_id, ignored, and the triangle fields feature_fields names; features on the rows of input: edges with Int64 edge_id, points with Int64 vertex_id, or Int64 vertex vertex_id (-1 for none) |
tetwild |
input; input_edges, input_points |
Triangle surfaces; optional curve/point geometry, no attributes |
triwild |
input; input_points |
Curve geometry, optional free points, no attributes |
simwild |
input; optional envelope for tagged input |
Surface/curve geometry, or tagged triangle/tet mesh |
topological_offset |
input; optional envelope |
Tagged triangle/tet mesh |
manifold_extraction |
input; optional envelope |
Tagged triangle/tet mesh |
prismatic_mesh |
input |
Tet construction mesh with corr_input_vid and cell region memberships |
c1_simplification |
surface_mesh, uv_mesh |
Surface triangle dofs (Float64, 36 columns), vertex is_cone (UInt8, 1 column); UV triangle geometry |
tracking |
none for info and features; transfer: input, optional tet_mesh; volume: optional tet_mesh and output_tet_mesh |
The history as bytes (option history, not a mesh), and for volume the volume history as bytes (option volume_history). transfer: the triangle surface the history starts from, whose triangle attributes it carries (uv, normal per corner, any other per face), no vertex attributes; the tet mesh the history ends at. volume: the tet meshes the volume history starts and ends at, with their tags if they have them |
For tagged inputs, tags on the top-dimensional cells names their regions, such as tag_0. A
column labelled ambient is the background, as is a cell without a tag; a mesh without a tags
field is all background. Tagged results label their background ambient explicitly; the shared
readers and writers infer nothing from an untagged row. Existing exported diagnostics have fixed allowed
names in the adapters and may be supplied again. Extra unrelated fields are errors. As on the
.msh route, a lower-dimensional block of the envelope's type (triangles in a tet mesh, edges in
a triangle mesh) is the envelope, e.g. simwild's EnvelopeSurface; it cannot be combined with an
envelope input. Other lower-dimensional blocks are rejected.
The prismatic adapter accepts scalar tag_0 and offset_tag, or the corresponding columns of
tags. vid defaults to vertex row IDs; absent labels are derived from input-volume cells and
correspondence. corr_input_vid names a source vid, with -1 for no correspondence. Existing
construction diagnostics are accepted. Enable save_offset_correspondence and disable offset
optimization to obtain construction correspondence for the offset-to-prismatic workflow.
C1's 36 DOF columns flatten its 12-by-3 face matrix in row order. UV vertices use V's first two
columns; UV faces correspond to surface face rows. Its unfinished tetrahedral mapping route is
unchanged.
Results and compatibility¶
mesh is the primary output key. Components also return their applicable existing secondary
geometry under fixed keys:
surface: the boundary (triangles in 3D; in 2D -- triwild, 2D simwild -- the boundary edges). Manifold extraction returns its extracted boundary here.edges: simwild's 3D feature edges; isotropic_remeshing's feature edges, withfeaturesorfeature_fields(its Int64edge_id, andmesh's Int64vertex_idand per-face fields -- in the types they were given in, with their labels -- are what its.vtuoutput holds, an edge block of none where there are none), which can be the next call'sfeatures.offset: the offset surface or front; prismatic's classified offset faces.envelope,features,uv, and manifold extraction's 3D input-complex previewinput_complex.- Prismatic
export_input_and_shelladdsinput_and_shelland its classified offset faces asinput_and_shell_offset; withprism_dominant, the report adds that export'sinput_and_shell_hybridreport, as in its_hybrid.jsonfile. history_input: simwild's, withhistory_output, the tagged tet mesh its surface history starts from (the file route's<output>_history_input.msh); tetwild's, withvolume_history_output, the tet mesh its volume history starts from (its<output>_history_input.msh).history_output: tetwild's, withvolume_history_output, the tet mesh its volume history ends at, all of it before the filter (its<output>_history_output.msh);meshis the part the filter keeps. For simwild the volume history ends atmesh.
For tetwild, triwild, simwild, topological_offset and isotropic_remeshing, surface, edges and
offset share the mesh vertex rows: their connectivity indexes the same V, with the same
vertex fields, so row i is the same vertex in every one of them, and vertices an output does not
reference are kept.
This is deliberate -- it maps a boundary vertex to its volume vertex without a search -- at the
cost of one vertex array per output. envelope, features, manifold extraction's surface and
prismatic's offset faces have vertices of their own.
The exporters' legacy file names map to these keys through one explicit table
(io::default_output_keys); an unmapped export or two exports with one key is an error, never
a silently dropped output. Reports contain the existing
algorithm counts, quality measures, and timings; prismatic reports also contain hybrid and
smoothing/remeshing reports. Report values are available without a report filename.
The existing topological-offset driver currently refuses 2D inputs; the memory overload preserves that restriction.
The file-oriented entry points retain existing configurations, relative-path conventions,
output filenames, intermediate exports, and report files. The tagged components (simwild, triwild,
topological_offset, manifold_extraction) build the same result on both routes: the file route
writes its mesh (the envelope stays out of the file) through io::write_mesh, and
reads its .msh input through io::read_mesh. An explicit per-call output policy
turns off their file observers for memory calls. Component entry points serialize their use of
the legacy process-wide loggers, temporarily using console sinks during memory calls and restoring
the previous logger afterward.
Component calls do not run concurrently. Every call -- file or memory, from C++ or Python -- holds
one process-wide lock for its whole duration, so calls from several threads run one after
another; the Python bindings release the GIL, but a second call still waits for the first. Use
num_threads for parallelism within a call. Existing algorithm options, filtering,
cleaning, and exact-arithmetic decisions are shared between the two routes.
Histories¶
isotropic_remeshing, qslim, shortest_edge_collapse, tetwild and simwild in 3D record the
history of their surface with history_output, and triwild the history of its curves, from
which tracking rebuilds the map between input and output (see docs/tracking.md). The file
route saves it to that file. The memory route writes no file: any non-empty history_output
turns the recording on, and the report returns the history as history, the bytes of the file
the file route would write -- a nlohmann::json binary value in C++. On the memory route the
history starts at the meshes given and ends at the meshes returned.
history::write_history(path, bytes) saves the bytes as that file, unchanged, and
history::read_history(path) reads them back, as io::write_mesh and io::read_mesh do for a
mesh; history::SurfaceHistory::from_bytes and history::CurveHistory::from_bytes load them,
and history::face_size(bytes) says which they hold (3 corners a face, or 2). The history file
is its own format, not a mesh.
tetwild and simwild in 3D also record the volume around the tracked surface with
volume_history_output (which needs history_output). The memory route returns it the same
way, as volume_history in the report, the bytes of its file, and writes no file; with it the
meshes the volume history names its points by, as output meshes: history_input, where it
starts, and tetwild's history_output, the mesh before its filter, where it ends (simwild's
ends at mesh). read_history and write_history take a volume history by its header as they
take the others; history::VolumeHistory::from_bytes loads it and history::is_volume_history
says that bytes are one.
tracking takes the bytes as history on the memory route, where a string is refused, as
every memory call refuses to take a name for a file. info returns its report, of a surface or
of a curve history. features, on a curve history, returns its feature points in the report:
feature_points, pairs of an input vertex and the output vertex it became, -1 for none (an
Int64 array of two columns in NumPy). transfer, on a surface history, needs
input, the mesh the history starts from -- the history's own parameters name meshes of an
earlier call, or files, neither of which the memory route reads -- and returns the output
surface as mesh, the carried fields as triangle attributes, per face or, for uv and
normal, three values per corner (uv: six columns), with input_face (Int64), the input face
each output face comes from. With tet_mesh, the tet mesh the history ends at, it also returns
that mesh as tet_mesh with the fields on the rows of its triangle block: a tracked face already
there (a face tag) on its row, any other on a row added after them. volume takes the volume
history's bytes as volume_history -- a string is refused there too -- and, to sample points
near the tracked surface and check where they land, tet_mesh and output_tet_mesh, keys of
the meshes given (a producer's history_input, and its mesh or history_output); its report
holds what the file route logs (stats, round_trip, regions_kept, ...). map and
map_volume, whose points are text files, are refused on the memory route: points are mapped
in memory by Tracking, CurveTracking and VolumeTracking
(components/tracking/.../tracking.hpp), in Python wm.Tracking, wm.CurveTracking and
wm.VolumeTracking.
In Python the history is bytes: the bindings return a report's binary values as bytes, and
pass bytes in the options as binary values (pybind11_json alone would make them base64 text).
wm.write_history(path, history) and wm.read_history(path) are the two file functions:
outputs, report = wm.isotropic_remeshing(
{"input": "surface", "history_output": "record"}, {"surface": surface})
wm.write_history("remeshed.wmtkhist", report["history"]) # the file the file route writes
history = wm.read_history("remeshed.wmtkhist") # the same bytes
t = wm.Tracking(history) # the map; wm.Tracking.load(path) from the file
ids, bary = t.to_output(faces, bary) # int64 (n,), float64 (n, 3): points on the input
faces, bary = t.to_input(ids, bary) # and back; -1 (and NaN) for a point with no image
A volume history maps the same way, with the surface history it was recorded with and the two meshes, as a memory call returns them:
outputs, report = wm.tetwild(
{"input": "surface", "history_output": "x", "volume_history_output": "y"}, {"surface": s})
v = wm.VolumeTracking(report["history"], report["volume_history"],
outputs["history_input"], outputs["history_output"])
tets, bary = v.to_output(tets, bary) # int64 (n,), float64 (n, 4): -1 and NaN for no tet
xyz, region = v.map_positions(xyz) # float64 (n, 3); direction="to_input", regions=...
wm.VolumeTracking(history, volume_history) maps positions only, and
wm.VolumeTracking.load(...) takes the four files instead.
wm.CurveTracking does the same for a curve history: segments of the input curve files (n,)
and coordinates (n, 2) to two vertices of the output mesh (n, 2), and back, and
feature_points(). Ids are (n,) faces or segments, or (n, 3) / (n, 2) vertices on a side
named by a mesh's vertices (output_by_vertices, input_by_vertices); warnings=True adds the
call's warnings as a third element. Building a map holds the process-wide lock, as its loading
logs; mapping releases the GIL and changes nothing -- a VolumeTracking only the operation maps
it keeps, under a lock of its own -- so threads may share one object.
docs/tracking.md has the details.
Shared file utilities¶
io::read_mesh(path) reads MSH, OBJ, OFF, STL and the existing libigl surface formats.
It preserves row identity and isolated vertices; cleaning belongs to component preprocessing.
Nonstandard MSH node tags are preserved as zero-based source IDs in vid, unless an explicit
vid field is supplied.
OBJ polylines retain orientation, and UV/normal values are represented per triangle corner
(uv: six columns; normal: nine columns), read only when every face corner references a value.
Existing component-specific VTU input remains
unchanged; the shared utilities do not add a VTU reader.
io::write_mesh(path, data) writes MSH or delegates VTU output to the existing
paraviewo::VTUWriter, without a new serializer or custom VTU metadata. VTU attributes use the
existing Float64 representation, with exact numeric conversion required. Attribute labels,
two-column fields (which the writer pads), names requiring XML escaping, and differing attribute
schemas across cell blocks are rejected. The writer rounds values below 1e-16 in magnitude to
zero, as in every component's own VTU output; this is reported as a warning. Use MSH when those
fields need to be preserved. MSH fields use
reversible scalar splitting with type/shape/label metadata in their data headers. Integer values
that cannot be represented exactly by MSH's Float64 field values are rejected. Physical groups
retain overlapping memberships. Undefined floating diagnostics are encoded reversibly in MSH
metadata. Unsupported cell types, encodings, and unrepresentable data throw rather than being
silently dropped.