The life of a component call¶
A component is a pair of C++ functions in wmtk::components::<name>. wmtk_app, the Python
module, the integration tests and C++ programs all call one of the two. This page follows a call
from the command line to the component's mesh and back, with
shortest edge collapse
as the example.
The two entry points¶
Every component declares both, in wmtk/components/<name>/<name>.hpp:
namespace wmtk::components::shortest_edge_collapse {
void shortest_edge_collapse(nlohmann::json json_params);
ComponentResult shortest_edge_collapse(nlohmann::json json_params, const MeshMap& meshes);
}
The first is the file route: inputs and outputs are files named in the options. The second is
the memory route: inputs are entries of meshes, nothing is written, and the outputs come
back as a ComponentResult, a pair of named output meshes and a JSON report
(src/wmtk/MeshData.hpp).
Both build an io::ComponentIO and call the same run():
void shortest_edge_collapse(nlohmann::json json_params)
{
io::ComponentIO context;
run(std::move(json_params), context);
}
ComponentResult shortest_edge_collapse(nlohmann::json json_params, const MeshMap& meshes)
{
io::ComponentIO context(&meshes);
run(std::move(json_params), context);
return context.take_result();
}
How the callers find the components¶
wmtk_app and the bindings name no component in their code. add_component(<name>)
(components/cmake/add_component.cmake)
appends one line per component to six headers in app/, which
components/CMakeLists.txt
empties at the start of every configure:
| Header | The line it gets | Included by |
|---|---|---|
components_include.hpp |
#include <wmtk/components/<name>/<name>.hpp> |
wmtk_app, the bindings, the integration tests |
components_map.hpp |
components_map["<name>"] = the file overload |
wmtk_app, the bindings, the integration tests |
components_memory_map.hpp |
memory_components["<name>"] = the memory overload |
the bindings |
components_spec_include.hpp |
#include <<name>_spec.hpp> |
wmtk_app, the bindings |
components_spec_map.hpp |
components_spec_map["<name>"] = the spec's accessor |
wmtk_app, the bindings |
components_python.hpp |
WMTK_PYTHON_BINDINGS(<name>), only if python/src/<name>_bindings.cpp exists |
the bindings |
The maps are filled where the headers are included, inside a function:
std::map<std::string, std::function<void(nlohmann::json)>> components_map;
// include auto-generated map
#include "components_map.hpp"
The headers are generated: edit add_component, not them, and reconfigure after adding a
component. add_component also links the component into wmtk_app, into the Python module
(with WMTK_PYBIND) and into wmtk_integration_tests (with WMTK_BUILD_INTEGRATION_TESTS).
wmtk_app¶
app/main.cpp takes
three options: -j <file>, --list and --options <name>. With -j it
- parses the JSON file, and requires an
applicationkey; - removes
$schema(an editor's, see JSON Schemas), and setsinput_dirto the directory of the JSON file, so that the component resolves the paths in the file against it; - looks the application up in
components_map(Application ... unknownotherwise); - warns about every top-level key the component's spec does not have (see below);
- calls the file overload with the JSON.
main does not catch what the component throws: a component fails with
log_and_throw_error, which logs the message, and the process ends on the uncaught exception.
Inside the component¶
run() takes the same steps in every component.
1. The route. component_io.prepare(json_params, "<name>") does nothing on the file route.
On the memory route it sets application, empties input_dir, output, log_file and
report, and, for simwild, tetwild and triwild, wraps a single input string into a list.
2. Validation and defaults. The component checks the JSON against its spec and fills in the defaults:
const auto spec =
jse::embed::wmtk_shortest_edge_collapse_spec::shortest_edge_collapse_spec::spec();
jse::JSE spec_engine;
bool r = spec_engine.verify_json(json_params, spec);
if (!r) {
log_and_throw_error(spec_engine.log2str());
}
json_params = spec_engine.inject_defaults(json_params, spec);
The spec is compiled in: add_component runs jse_add_spec on <name>_spec.json, which
generates <name>_spec.hpp, declaring the accessor above, and a source file holding the JSON,
both in generated/<name>/ in the build directory. The json-spec-engine
checks types, required keys and allowed values; with strict off, its default, it accepts keys
no rule covers, and the component then never reads them. Only topological_offset sets
spec_engine.strict = true and refuses them. tetwild, triwild and simwild do this step through
utils::verify_and_setup_logger
(src/wmtk/utils/DriverPrologue.hpp),
which also opens log_file.
3. The inputs. component_io.path(root, json_params["input"]) is the file path, resolved
against input_dir by utils::resolve_path (against the current directory when input_dir is
empty), on the file route, and the mesh's name on the memory route. component_io.triangles(...)
then reads the file, or takes the named MeshData and checks its attributes, and cleans both the
same way: duplicate vertices (within remove_duplicate_eps), degenerate faces and unreferenced
vertices go.
4. The mesh. The component builds its own mesh class from the arrays:
ShortestEdgeCollapse m(v, num_threads, !use_sample_envelope);
wmtk::set_preallocation_factor_from_json(m, json_params);
m.set_use_link_condition(use_link_condition);
m.create_mesh(v.size(), tri, modified_v, envelope_size, boundary_envelope_size);
5. The operation loop. Here m.collapse_shortest(target_verts), which runs one
ExecutePass over every edge (writing an operation). tetwild, triwild,
simwild and topological_offset run mesh_improvement() (the Wild driver).
6. The output. consolidate_mesh() compacts the mesh; the file route writes output (when
it is not empty: the memory route emptied it); the memory route fills component_io.result:
if (component_io.memory())
component_io.result.first["mesh"] =
io::triangle_data(m, [&](size_t v) { return m.vertex_attrs[v].pos; });
component_io.result.second = {
{"vertices", m.get_vertices().size()},
{"triangles", m.get_faces().size()}};
The memory route¶
io::ComponentIO
(src/wmtk/io/ComponentIO.hpp)
is the only place that knows which route a call is on (memory()); the rest of the component
asks it for inputs and hands it outputs. On the memory route:
- A selector (
"input": "surface") names an entry of theMeshMap. A missing one throwsUnknown input mesh; every one isvalidate()d; an attribute the component does not accept throwsUnknown mesh attribute. - Nothing is written. A recorded history goes into the report as bytes (
save_history, thentake_result()puts it under"history", and a volume history under"volume_history"). - The component logs to a console logger of its own, not to a file logger the caller set up.
On both routes, ComponentIO holds a process-wide mutex for the duration of the call: component
calls in one process run one at a time.
The contract of each component's inputs, the MeshData layout and the NumPy dictionaries are
specified in component I/O.
The Python bindings¶
The module wildmeshing._core is built from
python/src/pywildmeshing.cpp
and mesh_conversion.cpp; python/wildmeshing/__init__.py re-exports it. For every component
in memory_components, the module defines one function with two overloads:
wm.<name>(options, meshes): the memory route.call_component(mesh_conversion.cpp) converts the meshes the selectors name (the keysinput,input_edges,input_points,surface_mesh,uv_mesh,envelope,tet_mesh,output_tet_meshandfeatures), releases the GIL, calls the memory overload and returns(outputs, report)as NumPy dictionaries.wm.<name>(options): the file route, aswmtk_appruns it. It addsapplicationif it is missing and releases the GIL. It does not setinput_dir, so relative paths resolve against the current directory.
The docstring of each function is built from its spec, as wmtk_app --options prints it:
help(wm.qslim) lists every option. Next to the components:
wm.components(): the names of the components, from the spec map.wm.spec(name): a component's spec, the rules of its*_spec.json, as a Python list.wm.wildmeshing(options)andwm.wildmeshing(options, meshes): dispatch onoptions["application"], for code written against the older single entry point.
A component with Python classes or functions of its own puts them in
python/src/<name>_bindings.cpp, defining wmtk::python::bind_<name>(module); add_component
compiles the file into the module and lists it in components_python.hpp, and the module calls it.
simwild and tracking have one.
The spec map¶
app/component_specs.hpp
reads the specs for wmtk_app and the bindings: the root rule's doc (the rule with pointer
/), the top-level options in the spec's order, each option's type, default and the first
sentence of its doc.
wmtk_app --list prints every component with the first sentence of its root doc.
wmtk_app --options <name> prints the root doc, the URL of the component's reference page, and
one line per option:
$ wmtk_app --options shortest_edge_collapse
shortest_edge_collapse
Simplifies a triangle mesh by collapsing its shortest edges first (Hoppe 1996, Progressive Meshes), until the vertex count reaches the target. With a positive eps_rel, every collapse keeps the surface inside an envelope around the input.
Options (every one, with its full description: https://wildmeshing.github.io/wildmeshing-toolkit/reference/shortest_edge_collapse/):
application (string, required): Application name must be shortest_edge_collapse.
input (string, required): Triangular input mesh.
output (string, default "out.obj"): Output file name (with extension).
...
So a spec's root doc should open with a sentence that says what the component does, and each
option's doc with a sentence that stands alone. The full text of each doc goes into the
reference (writing documentation).
Unknown options¶
Since most components ignore a key their spec does not have, a misspelled option would silently
keep its default. component_specs::unknown_options lists the top-level keys that are not
options of the spec (input_dir and $schema excepted), and the callers warn about each:
wmtk_app:tetwild has no option `eps_rell` (wmtk_app --options tetwild lists them).- The bindings, on both overloads of
wm.<name>and ofwm.wildmeshing:tetwild has no option `eps_rell` (see help(wildmeshing.tetwild)).
The warning does not stop the run, and a direct C++ call does not warn. topological_offset, with its strict validation, then refuses the key.